blob: 69d8da838c4fbfe1d1b821678730a358376f0fb6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.dubbo.admin.common.util;
import org.apache.dubbo.admin.model.domain.Route;
import org.apache.dubbo.admin.model.dto.AccessDTO;
import org.apache.dubbo.admin.model.dto.ConditionRouteDTO;
import org.apache.dubbo.admin.model.dto.TagRouteDTO;
import org.apache.dubbo.admin.model.store.RoutingRule;
import org.apache.dubbo.admin.model.store.TagRoute;
import org.apache.dubbo.common.utils.StringUtils;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Router rule can be divided into two parts, When Condition and Then Condition <br>
* When/Then Confition is expressed in a style of (KV) pair, the V part of the condition pair can contain multiple values (a list) <br>
* The meaning of Rule: If a request matches When Condition, then use Then Condition to filter providers (only providers match Then Condition will be returned). <br>
* The process of using Conditions to match consumers and providers is called `Filter`.
* When Condition are used to filter ConsumersController, while Then Condition are used to filter ProvidersController.
* RouteUtils performs like this: If a Consumer matches When Condition, then only return the ProvidersController matches Then Condition. This means RouteUtils should be applied to current Consumer and the providers returned are filtered by RouteUtils.<br>
*
* An example of ConditionRoute Rule:<code>
* key1 = value11,value12 & key2 = value21 & key2 != value22 => key3 = value3 & key4 = value41,vlaue42 & key5 !=value51
* </code>。
* The part before <code>=></code> is called When Condition, it's a KV pair; the follower part is Then Condition, also a KV pair. V part in KV can have more than one value, separated by ','<br><br>
*
* Value object, thread safe.
*
*/
public class RouteUtils {
@SuppressWarnings("unchecked")
static RouteUtils EMPTY = new RouteUtils(Collections.EMPTY_MAP, Collections.EMPTY_MAP);
private static Pattern ROUTE_PATTERN = Pattern.compile("([&!=,]*)\\s*([^&!=,\\s]+)");
private static Pattern CONDITION_SEPERATOR = Pattern.compile("(.*)=>(.*)");
private static Pattern VALUE_LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
final Map<String, MatchPair> whenCondition;
final Map<String, MatchPair> thenCondition;
private volatile String tostring = null;
// FIXME
private RouteUtils(Map<String, MatchPair> when, Map<String, MatchPair> then) {
for (Map.Entry<String, MatchPair> entry : when.entrySet()) {
entry.getValue().freeze();
}
for (Map.Entry<String, MatchPair> entry : then.entrySet()) {
entry.getValue().freeze();
}
// NOTE: Both When Condition and Then Condition can be null
this.whenCondition = when;
this.thenCondition = then;
}
public static Map<String, MatchPair> parseRule(String rule)
throws ParseException {
Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
if (StringUtils.isBlank(rule)) {
return condition;
}
// K-V pair, contains matches part and mismatches part
MatchPair pair = null;
// V part has multiple values
Set<String> values = null;
final Matcher matcher = ROUTE_PATTERN.matcher(rule);
while (matcher.find()) { // match one by one
String separator = matcher.group(1);
String content = matcher.group(2);
// The expression starts
if (separator == null || separator.length() == 0) {
pair = new MatchPair();
condition.put(content, pair);
}
// The KV starts
else if ("&".equals(separator)) {
if (condition.get(content) == null) {
pair = new MatchPair();
condition.put(content, pair);
} else {
condition.put(content, pair);
}
}
// The Value part of KV starts
else if ("=".equals(separator)) {
if (pair == null)
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
values = pair.matches;
values.add(content);
}
// The Value part of KV starts
else if ("!=".equals(separator)) {
if (pair == null)
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
values = pair.unmatches;
values.add(content);
}
// The Value part of KV has multiple values, separated by ','
else if (",".equals(separator)) { // separated by ','
if (values == null || values.size() == 0)
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
values.add(content);
} else {
throw new ParseException("Illegal route rule \"" + rule
+ "\", The error char '" + separator + "' at index "
+ matcher.start() + " before \"" + content + "\".", matcher.start());
}
}
return condition;
}
/**
* Parse the RouteUtils as a string into an object.
*
* @throws ParseException RouteUtils string format is wrong. The following input conditions, RouteUtils are illegal.
* <ul> <li> input is <code>null</code>。
* <li> input is "" or " "。
* <li> input Rule doesn't have a When Condition
* <li> input Rule doesn't have a Then Condition
* </ul>
*/
public static RouteUtils parse(Route conditionRoute) throws ParseException {
if (conditionRoute == null)
throw new ParseException("null conditionRoute!", 0);
if (conditionRoute.getMatchRule() == null && conditionRoute.getFilterRule() == null) {
return parse(conditionRoute.getRule());
}
return parse(conditionRoute == null ? null : conditionRoute.getMatchRule(), conditionRoute == null ? null : conditionRoute.getFilterRule());
}
public static RouteUtils parse(String whenRule, String thenRule) throws ParseException {
/*if (whenRule == null || whenRule.trim().length() == 0) {
throw new ParseException("Illegal route rule without when express", 0);
}*/
if (thenRule == null || thenRule.trim().length() == 0) {
throw new ParseException("Illegal route rule without then express", 0);
}
Map<String, MatchPair> when = parseRule(whenRule.trim());
Map<String, MatchPair> then = parseRule(thenRule.trim());
return new RouteUtils(when, then);
}
public static RouteUtils parse(String rule) throws ParseException {
if (StringUtils.isBlank(rule)) {
throw new ParseException("Illegal blank route rule", 0);
}
final Matcher matcher = CONDITION_SEPERATOR.matcher(rule);
if (!matcher.matches()) throw new ParseException("condition seperator => not found!", 0);
return parse(matcher.group(1), matcher.group(2));
}
/**
* @see #parse(String)
* @throws RuntimeException This is an wrapper exception for the {@link ParseException} thrown by the {@link #parse (String)} method.
*/
public static RouteUtils parseQuitely(Route conditionRoute) {
try {
return parse(conditionRoute);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
public static TagRoute convertTagroutetoStore(TagRouteDTO tagRoute) {
TagRoute store = new TagRoute();
store.setKey(tagRoute.getApplication());
store.setEnabled(tagRoute.isEnabled());
store.setForce(tagRoute.isForce());
store.setPriority(tagRoute.getPriority());
store.setRuntime(tagRoute.isRuntime());
store.setTags(tagRoute.getTags());
return store;
}
public static TagRouteDTO convertTagroutetoDisplay(TagRoute tagRoute) {
TagRouteDTO tagRouteDTO = new TagRouteDTO();
tagRouteDTO.setApplication(tagRoute.getKey());
tagRouteDTO.setRuntime(tagRoute.isRuntime());
tagRouteDTO.setPriority(tagRoute.getPriority());
tagRouteDTO.setTags(tagRoute.getTags());
tagRouteDTO.setForce(tagRoute.isForce());
tagRouteDTO.setEnabled(tagRoute.isEnabled());
return tagRouteDTO;
}
public static RoutingRule insertConditionRule(RoutingRule existRule, ConditionRouteDTO conditionRoute) {
if (existRule == null) {
existRule = new RoutingRule();
if (StringUtils.isNotEmpty(conditionRoute.getApplication())) {
existRule.setKey(conditionRoute.getApplication());
existRule.setScope(Constants.APPLICATION);
} else {
existRule.setKey(conditionRoute.getService().replace("/", "*"));
existRule.setScope(Constants.SERVICE);
}
}
existRule.setConditions(conditionRoute.getConditions());
existRule.setEnabled(conditionRoute.isEnabled());
existRule.setForce(conditionRoute.isForce());
existRule.setRuntime(conditionRoute.isRuntime());
existRule.setPriority(conditionRoute.getPriority());
return existRule;
}
public static List<String> convertToBlackWhiteList(AccessDTO accessDTO) {
if (accessDTO == null) {
return null;
}
Set<String> whiteList = accessDTO.getWhitelist();
Set<String> blackList = accessDTO.getBlacklist();
List<String> conditions = new ArrayList<>();
if (whiteList != null && whiteList.size() > 0) {
StringBuilder sb = new StringBuilder();
sb.append("host != ");
for (String white : whiteList) {
sb.append(white).append(",");
}
sb.deleteCharAt(sb.length() - 1);
sb.append(" =>");
conditions.add(sb.toString());
}
if (blackList != null && blackList.size() > 0) {
StringBuilder sb = new StringBuilder();
sb.append("host = ");
for (String black : blackList) {
sb.append(black).append(",");
}
sb.deleteCharAt(sb.length() - 1);
sb.append(" =>");
conditions.add(sb.toString());
}
return conditions;
}
public static List<String> filterBlackWhiteListFromConditions(List<String> conditions) {
List<String> result = new ArrayList<>();
if (conditions == null || conditions.isEmpty()) {
return result;
}
for (String condition : conditions) {
if (isBlackList(condition)) {
result.add(condition);
} else if (isWhiteList(condition)) {
result.add(condition);
}
}
return result;
}
/**
* the function of "black white" is part of the function of condition route
* @param conditions
* @return
*/
public static List<String> filterConditionsExcludeBlackWhiteList(List<String> conditions) {
List<String> result = new ArrayList<>();
if (conditions == null || conditions.isEmpty()) {
return result;
}
for (String condition : conditions) {
if (!isBlackList(condition) && !isWhiteList(condition)) {
result.add(condition);
}
}
return result;
}
/**
* the function of "black white" is part of the function of condition route
* @param conditions
* @return
*/
public static List<String> filterConditionRuleFromConditions(List<String> conditions) {
List<String> result = new ArrayList<>();
if (conditions == null || conditions.isEmpty()) {
return result;
}
for (String condition : conditions) {
result.add(condition);
}
return result;
}
public static List<String> removeBlackWhiteListRuleFromConditions(List<String> conditions) {
List<String> result = new ArrayList<>();
if (conditions == null || conditions.isEmpty()) {
return result;
}
for (String condition : conditions) {
if (!isBlackList(condition) && !isWhiteList(condition)) {
result.add(condition);
}
}
return result;
}
public static AccessDTO convertToAccessDTO(List<String> blackWhiteList, String scope, String key) {
if (blackWhiteList == null) {
return null;
}
AccessDTO accessDTO = new AccessDTO();
if (scope.equals(Constants.APPLICATION)) {
accessDTO.setApplication(key);
} else {
ConvertUtil.detachIdToService(key, accessDTO);
}
if (blackWhiteList != null) {
for (String condition : blackWhiteList) {
if (condition.contains("host != ")) {
//white list
condition = org.apache.commons.lang3.StringUtils.substringBetween(condition, "host !=", " =>").trim();
accessDTO.setWhitelist(new HashSet<>(Arrays.asList(condition.split(","))));
}
if (condition.contains("host = ")) {
//black list
condition = org.apache.commons.lang3.StringUtils.substringBetween(condition, "host =", " =>").trim();
accessDTO.setBlacklist(new HashSet<>(Arrays.asList(condition.split(","))));
}
}
}
return accessDTO;
}
public static Route convertAccessDTOtoRoute(AccessDTO accessDTO) {
Route route = new Route();
route.setService(ConvertUtil.getIdFromDTO(accessDTO));
route.setForce(true);
route.setFilterRule("false");
route.setEnabled(true);
Map<String, RouteUtils.MatchPair> when = new HashMap<>();
RouteUtils.MatchPair matchPair = new RouteUtils.MatchPair(new HashSet<>(), new HashSet<>());
when.put(Route.KEY_CONSUMER_HOST, matchPair);
if (accessDTO.getWhitelist() != null) {
matchPair.getUnmatches().addAll(accessDTO.getWhitelist());
}
if (accessDTO.getBlacklist() != null) {
matchPair.getMatches().addAll(accessDTO.getBlacklist());
}
StringBuilder sb = new StringBuilder();
RouteUtils.contidionToString(sb, when);
route.setMatchRule(sb.toString());
return route;
}
public static ConditionRouteDTO createConditionRouteFromRule(RoutingRule routingRule) {
ConditionRouteDTO conditionRouteDTO = new ConditionRouteDTO();
if (Constants.SERVICE.equals(routingRule.getScope())) {
conditionRouteDTO.setService(routingRule.getKey());
} else {
conditionRouteDTO.setApplication(routingRule.getKey());
}
conditionRouteDTO.setConditions(RouteUtils.filterConditionRuleFromConditions(routingRule.getConditions()));
conditionRouteDTO.setPriority(routingRule.getPriority());
conditionRouteDTO.setEnabled(routingRule.isEnabled());
conditionRouteDTO.setForce(routingRule.isForce());
conditionRouteDTO.setRuntime(routingRule.isRuntime());
return conditionRouteDTO;
}
public static Route convertBlackWhiteListtoRoute(List<String> blackWhiteList, String scope, String key) {
AccessDTO accessDTO = convertToAccessDTO(blackWhiteList, scope, key);
return convertAccessDTOtoRoute(accessDTO);
}
static Map<String, MatchPair> parseNameAndValueListString2Condition(Map<String, String> params, Map<String, String> notParams) {
Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
for (Entry<String, String> entry : params.entrySet()) {
String valueListString = entry.getValue();
if (StringUtils.isBlank(valueListString)) {
continue;
}
String[] list = VALUE_LIST_SEPARATOR.split(valueListString);
Set<String> set = new HashSet<String>();
for (String item : list) {
if (StringUtils.isBlank(item)) {
continue;
}
set.add(item.trim());
}
if (set.isEmpty()) {
continue;
}
String key = entry.getKey();
MatchPair matchPair = condition.get(key);
if (null == matchPair) {
matchPair = new MatchPair();
condition.put(key, matchPair);
}
matchPair.matches = set;
}
for (Entry<String, String> entry : notParams.entrySet()) {
String valueListString = entry.getValue();
if (StringUtils.isBlank(valueListString)) {
continue;
}
String[] list = VALUE_LIST_SEPARATOR.split(valueListString);
Set<String> set = new HashSet<String>();
for (String item : list) {
if (StringUtils.isBlank(item)) {
continue;
}
set.add(item.trim());
}
if (set.isEmpty()) {
continue;
}
String key = entry.getKey();
MatchPair matchPair = condition.get(key);
if (null == matchPair) {
matchPair = new MatchPair();
condition.put(key, matchPair);
}
matchPair.unmatches = set;
}
return condition;
}
public static RouteUtils createFromNameAndValueListString(Map<String, String> whenParams, Map<String, String> notWhenParams,
Map<String, String> thenParams, Map<String, String> notThenParams) {
Map<String, MatchPair> when = parseNameAndValueListString2Condition(whenParams, notWhenParams);
Map<String, MatchPair> then = parseNameAndValueListString2Condition(thenParams, notThenParams);
return new RouteUtils(when, then);
}
public static RouteUtils createFromCondition(Map<String, MatchPair> whenCondition, Map<String, MatchPair> thenCondition) {
return new RouteUtils(whenCondition, thenCondition);
}
public static RouteUtils copyWithRemove(RouteUtils copy, Set<String> whenParams, Set<String> thenParams) {
Map<String, MatchPair> when = new HashMap<String, MatchPair>();
for (Entry<String, MatchPair> entry : copy.getWhenCondition().entrySet()) {
if (whenParams == null || !whenParams.contains(entry.getKey())) {
when.put(entry.getKey(), entry.getValue());
}
}
Map<String, MatchPair> then = new HashMap<String, MatchPair>();
for (Entry<String, MatchPair> entry : copy.getThenCondition().entrySet()) {
if (thenParams == null || !thenParams.contains(entry.getKey())) {
then.put(entry.getKey(), entry.getValue());
}
}
return new RouteUtils(when, then);
}
/**
* Replace with the new condition value.
*
* @param copy Replace Base
* @param whenCondition WhenCondition to replace, if Base does not have an item, insert it directly.
* @param thenCondition ThenCondition to replace, if Base has no items, then insert directly.
* @return RouteUtils after replacement
*/
public static RouteUtils copyWithReplace(RouteUtils copy, Map<String, MatchPair> whenCondition, Map<String, MatchPair> thenCondition) {
if (null == copy) {
throw new NullPointerException("Argument copy is null!");
}
Map<String, MatchPair> when = new HashMap<String, MatchPair>();
when.putAll(copy.getWhenCondition());
if (whenCondition != null) {
when.putAll(whenCondition);
}
Map<String, MatchPair> then = new HashMap<String, MatchPair>();
then.putAll(copy.getThenCondition());
if (thenCondition != null) {
then.putAll(thenCondition);
}
return new RouteUtils(when, then);
}
// TODO ToString out of the current list is out of order, should we sort?
static void join(StringBuilder sb, Set<String> valueSet) {
boolean isFirst = true;
for (String s : valueSet) {
if (isFirst) {
isFirst = false;
} else {
sb.append(",");
}
sb.append(s);
}
}
/**
* Whether the sample passed the conditions.
* <p>
* If there is a Key in the KV for the sample, there is a corresponding MatchPair, and Value does not pass through MatchPair; {@code false} is returned; otherwise, {@code true} is returned.
*
* @see MatchPair#pass(String)
*/
public static boolean matchCondition(Map<String, String> sample,
Map<String, MatchPair> condition) {
for (Map.Entry<String, String> entry : sample.entrySet()) {
String key = entry.getKey();
MatchPair pair = condition.get(key);
if (pair != null && !pair.pass(entry.getValue())) {
return false;
}
}
return true;
}
// FIXME Remove such method calls
public static String join(Set<String> valueSet) {
StringBuilder sb = new StringBuilder(128);
join(sb, valueSet);
return sb.toString();
}
// TODO At present, the multiple Key of Condition is in disorder. Should we sort it?
public static void contidionToString(StringBuilder sb, Map<String, MatchPair> condition) {
boolean isFirst = true;
for (Entry<String, MatchPair> entry : condition.entrySet()) {
String keyName = entry.getKey();
MatchPair p = entry.getValue();
@SuppressWarnings("unchecked")
Set<String>[] setArray = new Set[]{p.matches, p.unmatches};
String[] opArray = {" = ", " != "};
for (int i = 0; i < setArray.length; ++i) {
if (setArray[i].isEmpty()) {
continue;
}
if (isFirst) {
isFirst = false;
} else {
sb.append(" & ");
}
sb.append(keyName);
sb.append(opArray[i]);
join(sb, setArray[i]);
}
}
}
public boolean isWhenContainValue(String key, String value) {
MatchPair matchPair = whenCondition.get(key);
if (null == matchPair) {
return false;
}
return matchPair.containeValue(value);
}
public boolean isThenContainValue(String key, String value) {
MatchPair matchPair = thenCondition.get(key);
if (null == matchPair) {
return false;
}
return matchPair.containeValue(value);
}
public boolean isContainValue(String key, String value) {
return isWhenContainValue(key, value) || isThenContainValue(key, value);
}
public Map<String, MatchPair> getWhenCondition() {
return whenCondition;
}
public Map<String, MatchPair> getThenCondition() {
return thenCondition;
}
public String getWhenConditionString() {
StringBuilder sb = new StringBuilder(512);
contidionToString(sb, whenCondition);
return sb.toString();
}
public String getThenConditionString() {
StringBuilder sb = new StringBuilder(512);
contidionToString(sb, thenCondition);
return sb.toString();
}
private static boolean isBlackList(String address) {
return (address.startsWith("host = ") && address.endsWith(" =>"));
}
private static boolean isWhiteList(String address) {
return (address.startsWith("host != ") && address.endsWith(" =>"));
}
@Override
public String toString() {
if (tostring != null)
return tostring;
StringBuilder sb = new StringBuilder(512);
contidionToString(sb, whenCondition);
sb.append(" => ");
contidionToString(sb, thenCondition);
return tostring = sb.toString();
}
// Automatic generation with Eclipse
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((thenCondition == null) ? 0 : thenCondition.hashCode());
result = prime * result + ((whenCondition == null) ? 0 : whenCondition.hashCode());
return result;
}
// Automatic generation with Eclipse
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RouteUtils other = (RouteUtils) obj;
if (thenCondition == null) {
if (other.thenCondition != null)
return false;
} else if (!thenCondition.equals(other.thenCondition))
return false;
if (whenCondition == null) {
if (other.whenCondition != null)
return false;
} else if (!whenCondition.equals(other.whenCondition))
return false;
return true;
}
public static class MatchPair {
Set<String> matches = new HashSet<String>();
Set<String> unmatches = new HashSet<String>();
private volatile boolean freezed = false;
public MatchPair() {
}
public MatchPair(Set<String> matches, Set<String> unmatches) {
if (matches == null || unmatches == null) {
throw new IllegalArgumentException("argument of MatchPair is null!");
}
this.matches = matches;
this.unmatches = unmatches;
}
public Set<String> getMatches() {
return matches;
}
public Set<String> getUnmatches() {
return unmatches;
}
public MatchPair copy() {
MatchPair ret = new MatchPair();
ret.matches.addAll(matches);
ret.unmatches.addAll(unmatches);
return ret;
}
void freeze() {
if (freezed) return;
synchronized (this) {
if (freezed) return;
matches = Collections.unmodifiableSet(matches);
unmatches = Collections.unmodifiableSet(unmatches);
}
}
public boolean containeValue(String value) {
return matches.contains(value) || unmatches.contains(value);
}
/**
* Whether a given value is matched by the {@link MatchPair}.
* return {@code false}, if
* <ol>
* <li>value is in unmatches
* <li>matches is not null, but value is not in matches.
* </ol>
* otherwise, return<code>true</code>。
*/
public boolean pass(String sample) {
if (unmatches.contains(sample)) return false;
if (matches.isEmpty()) return true;
return matches.contains(sample);
}
@Override
public String toString() {
return String.format("{matches=%s,unmatches=%s}", matches.toString(), unmatches.toString());
}
// Automatic generation with Eclipse
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((matches == null) ? 0 : matches.hashCode());
result = prime * result + ((unmatches == null) ? 0 : unmatches.hashCode());
return result;
}
// Automatic generation with Eclipse
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MatchPair other = (MatchPair) obj;
if (matches == null) {
if (other.matches != null)
return false;
} else if (!matches.equals(other.matches))
return false;
if (unmatches == null) {
if (other.unmatches != null)
return false;
} else if (!unmatches.equals(other.unmatches))
return false;
return true;
}
}
}