[3.0-URL] Refactor URLParam with static reference (#7353)

* refact URLParam

* impl unsupported method

* fix NPE

* performance tuning

* add ut

* introduce constant

* remove unnecessary new

* change value to array

* add comment
fix default value not changed

* fix array outbound

* add empty constructor for URLParam to adapt hessian deserialize
This commit is contained in:
Albumen Kevin 2021-04-25 15:59:22 +08:00 committed by GitHub
parent ea933320e1
commit b9bca6a089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1238 additions and 80 deletions

View File

@ -177,7 +177,7 @@ class URL implements Serializable {
}
this.urlAddress = new PathURLAddress(protocol, username, password, path, host, port);
this.urlParam = new URLParam(parameters);
this.urlParam = URLParam.parse(parameters);
}
protected URL(String protocol,
@ -194,7 +194,7 @@ class URL implements Serializable {
}
this.urlAddress = new PathURLAddress(protocol, username, password, path, host, port);
this.urlParam = new URLParam(parameters);
this.urlParam = URLParam.parse(parameters);
}
public static URL cacheableValueOf(String url) {

View File

@ -45,7 +45,7 @@ public class ServiceConfigURL extends URL {
int port,
String path,
Map<String, String> parameters) {
this(new PathURLAddress(protocol, username, password, path, host, port), new URLParam(parameters), null);
this(new PathURLAddress(protocol, username, password, path, host, port), URLParam.parse(parameters), null);
}
public ServiceConfigURL(String protocol,
@ -56,7 +56,7 @@ public class ServiceConfigURL extends URL {
String path,
Map<String, String> parameters,
Map<String, Object> attributes) {
this(new PathURLAddress(protocol, username, password, path, host, port), new URLParam(parameters), attributes);
this(new PathURLAddress(protocol, username, password, path, host, port), URLParam.parse(parameters), attributes);
}
protected <T extends URL> T newURL(URLAddress urlAddress, URLParam urlParam) {

View File

@ -18,45 +18,125 @@ package org.apache.dubbo.common.url.component;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.URLStrParser;
import org.apache.dubbo.common.url.component.param.DynamicParamTable;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.eclipse.collections.impl.map.mutable.UnifiedMap;
import java.io.Serializable;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.Set;
import java.util.StringJoiner;
import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_KEY_PREFIX;
import static org.apache.dubbo.common.constants.CommonConstants.METHODS_KEY;
/**
* A class which store parameters for {@link URL}
* <br/>
* Using {@link DynamicParamTable} to compress common keys (i.e. side, version)
* <br/>
* {@link DynamicParamTable} allow to use only two integer value named `key` and
* `value-offset` to find a unique string to string key-pair. Also, `value-offset`
* is not required if the real value is the default value.
* <br/>
* URLParam should operate as Copy-On-Write, each modify actions will return a new Object
*
* @since 3.0
*/
public class URLParam implements Serializable {
private static final long serialVersionUID = -1985165475234910535L;
/**
* Maximum size of key-pairs requested using array moving to add into URLParam.
* If user request like addParameter for only one key-pair, adding value into a array
* on moving is more efficient. However when add more than ADD_PARAMETER_ON_MOVE_THRESHOLD
* size of key-pairs, recover compressed array back to map can reduce operation count
* when putting objects.
*/
private static final int ADD_PARAMETER_ON_MOVE_THRESHOLD = 1;
/**
* the original parameters string, empty if parameters have been modified or init by {@link Map}
*/
private final String rawParam;
private final Map<String, String> params;
/**
* using bit to save if index exist even if value is default value
*/
private final BitSet KEY;
/**
* using bit to save if value is default value (reduce VALUE size)
*/
private final BitSet DEFAULT_KEY;
/**
* a compressed (those key not exist or value is default value are bring removed in this array) array which contains value-offset
*/
private final int[] VALUE;
/**
* store extra parameters which key not match in {@link DynamicParamTable}
*/
private final Map<String, String> EXTRA_PARAMS;
//cache
private transient Map<String, Map<String, String>> methodParameters;
private transient long timestamp;
public URLParam(Map<String, String> params) {
this(params, null);
private final static URLParam EMPTY_PARAM = new URLParam(new BitSet(0), Collections.emptyMap(), Collections.emptyMap(), "");
private URLParam() {
this.rawParam = null;
this.KEY = null;
this.DEFAULT_KEY = null;
this.VALUE = null;
this.EXTRA_PARAMS = null;
}
public URLParam(Map<String, String> params, String rawParam) {
this.params = Collections.unmodifiableMap((params == null ? new HashMap<>() : new HashMap<>(params)));
private URLParam(BitSet key, Map<Integer, Integer> value, Map<String, String> extraParams, String rawParam) {
this.KEY = key;
this.DEFAULT_KEY = new BitSet(KEY.size());
this.VALUE = new int[value.size()];
// compress VALUE
for (int i = key.nextSetBit(0), offset = 0; i >= 0; i = key.nextSetBit(i + 1)) {
if (value.containsKey(i)) {
VALUE[offset++] = value.get(i);
} else {
DEFAULT_KEY.set(i);
}
}
this.EXTRA_PARAMS = Collections.unmodifiableMap((extraParams == null ? new HashMap<>() : new HashMap<>(extraParams)));
this.rawParam = rawParam;
this.timestamp = System.currentTimeMillis();
}
private URLParam(BitSet key, BitSet defaultKey, int[] value, Map<String, String> extraParams, String rawParam) {
this.KEY = key;
this.DEFAULT_KEY = defaultKey;
this.VALUE = value;
this.EXTRA_PARAMS = Collections.unmodifiableMap((extraParams == null ? new HashMap<>() : new HashMap<>(extraParams)));
this.rawParam = rawParam;
this.timestamp = System.currentTimeMillis();
}
public Map<String, Map<String, String>> getMethodParameters() {
// TODO: delete it
if (methodParameters == null) {
methodParameters = Collections.unmodifiableMap(initMethodParameters(this.params));
methodParameters = Collections.unmodifiableMap(initMethodParameters(getParameters()));
}
return methodParameters;
}
@ -94,45 +174,242 @@ public class URLParam implements Serializable {
return methodParameters;
}
/**
* An embedded Map adapt to URLParam
* <br/>
* copy-on-write mode, urlParam reference will be changed after modify actions.
* If wishes to get the result after modify, please use {@link URLParamMap#getUrlParam()}
*/
public static class URLParamMap implements Map<String, String> {
private URLParam urlParam;
public URLParamMap(URLParam urlParam) {
this.urlParam = urlParam;
}
public static class Node implements Map.Entry<String, String> {
private final String key;
private String value;
public Node(String key, String value) {
this.key = key;
this.value = value;
}
@Override
public String getKey() {
return key;
}
@Override
public String getValue() {
return value;
}
@Override
public String setValue(String value) {
throw new UnsupportedOperationException();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Node node = (Node) o;
return Objects.equals(key, node.key) && Objects.equals(value, node.value);
}
@Override
public int hashCode() {
return Objects.hash(key, value);
}
}
@Override
public int size() {
return urlParam.KEY.cardinality() + urlParam.EXTRA_PARAMS.size();
}
@Override
public boolean isEmpty() {
return size() == 0;
}
@Override
public boolean containsKey(Object key) {
if (key instanceof String) {
return urlParam.hasParameter((String) key);
} else {
return false;
}
}
@Override
public boolean containsValue(Object value) {
return values().contains(value);
}
@Override
public String get(Object key) {
if (key instanceof String) {
return urlParam.getParameter((String) key);
} else {
return null;
}
}
@Override
public String put(String key, String value) {
String previous = urlParam.getParameter(key);
urlParam = urlParam.addParameter(key, value);
return previous;
}
@Override
public String remove(Object key) {
if (key instanceof String) {
String previous = urlParam.getParameter((String) key);
urlParam = urlParam.removeParameters((String) key);
return previous;
} else {
return null;
}
}
@Override
public void putAll(Map<? extends String, ? extends String> m) {
urlParam = urlParam.addParameters((Map<String, String>) m);
}
@Override
public void clear() {
urlParam = urlParam.clearParameters();
}
@Override
public Set<String> keySet() {
Set<String> set = new HashSet<>((int) ((urlParam.VALUE.length + urlParam.EXTRA_PARAMS.size()) / 0.75) + 1);
for (int i = urlParam.KEY.nextSetBit(0); i >= 0; i = urlParam.KEY.nextSetBit(i + 1)) {
set.add(DynamicParamTable.getKey(i));
}
for (Entry<String, String> entry : urlParam.EXTRA_PARAMS.entrySet()) {
set.add(entry.getKey());
}
return Collections.unmodifiableSet(set);
}
@Override
public Collection<String> values() {
Set<String> set = new HashSet<>((int) ((urlParam.VALUE.length + urlParam.EXTRA_PARAMS.size()) / 0.75) + 1);
for (int i = urlParam.KEY.nextSetBit(0); i >= 0; i = urlParam.KEY.nextSetBit(i + 1)) {
String value;
if (urlParam.DEFAULT_KEY.get(i)) {
value = DynamicParamTable.getDefaultValue(i);
} else {
int offset = urlParam.keyIndexToOffset(i);
value = DynamicParamTable.getValue(i, offset);
}
set.add(value);
}
for (Entry<String, String> entry : urlParam.EXTRA_PARAMS.entrySet()) {
set.add(entry.getValue());
}
return Collections.unmodifiableSet(set);
}
@Override
public Set<Entry<String, String>> entrySet() {
Set<Entry<String, String>> set = new HashSet<>((int) ((urlParam.KEY.cardinality() + urlParam.EXTRA_PARAMS.size()) / 0.75) + 1);
for (int i = urlParam.KEY.nextSetBit(0); i >= 0; i = urlParam.KEY.nextSetBit(i + 1)) {
String value;
if (urlParam.DEFAULT_KEY.get(i)) {
value = DynamicParamTable.getDefaultValue(i);
} else {
int offset = urlParam.keyIndexToOffset(i);
value = DynamicParamTable.getValue(i, offset);
}
set.add(new Node(DynamicParamTable.getKey(i), value));
}
for (Entry<String, String> entry : urlParam.EXTRA_PARAMS.entrySet()) {
set.add(new Node(entry.getKey(), entry.getValue()));
}
return Collections.unmodifiableSet(set);
}
public URLParam getUrlParam() {
return urlParam;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
URLParamMap that = (URLParamMap) o;
return Objects.equals(urlParam, that.urlParam);
}
@Override
public int hashCode() {
return Objects.hash(urlParam);
}
}
/**
* Get a Map like URLParam
*
* @return a {@link URLParamMap} adapt to URLParam
*/
public Map<String, String> getParameters() {
return params;
return new URLParamMap(this);
}
/**
* Add parameters to a new URLParam.
*
* @param key key
* @param value value
* @return A new URLParam
*/
public URLParam addParameter(String key, String value) {
if (StringUtils.isEmpty(key)
|| StringUtils.isEmpty(value)) {
if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) {
return this;
}
// if value doesn't change, return immediately
if (value.equals(getParameters().get(key))) { // value != null
return this;
}
Map<String, String> map = new HashMap<>(getParameters());
map.put(key, value);
return new URLParam(map, rawParam);
return addParameters(Collections.singletonMap(key, value));
}
/**
* Add absent parameters to a new URLParam.
*
* @param key key
* @param value value
* @return A new URLParam
*/
public URLParam addParameterIfAbsent(String key, String value) {
if (StringUtils.isEmpty(key)
|| StringUtils.isEmpty(value)) {
if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value)) {
return this;
}
if (hasParameter(key)) {
return this;
}
Map<String, String> map = new HashMap<>(getParameters());
map.put(key, value);
return new URLParam(map, rawParam);
return addParametersIfAbsent(Collections.singletonMap(key, value));
}
/**
* Add parameters to a new url.
* Add parameters to a new URLParam.
* If key-pair is present, this will cover it.
*
* @param parameters parameters in key-value pairs
* @return A new URL
* @return A new URLParam
*/
public URLParam addParameters(Map<String, String> parameters) {
if (CollectionUtils.isEmptyMap(parameters)) {
@ -159,53 +436,271 @@ public class URLParam implements Serializable {
return this;
}
Map<String, String> map = new HashMap<>((int)(getParameters().size() + parameters.size() / 0.75f) + 1);
map.putAll(getParameters());
map.putAll(parameters);
return new URLParam(map, rawParam);
return doAddParameters(parameters, false);
}
/**
* Add absent parameters to a new URLParam.
*
* @param parameters parameters in key-value pairs
* @return A new URL
*/
public URLParam addParametersIfAbsent(Map<String, String> parameters) {
if (CollectionUtils.isEmptyMap(parameters)) {
return this;
}
Map<String, String> map = new HashMap<>((int)(getParameters().size() + parameters.size() / 0.75f) + 1);
map.putAll(parameters);
map.putAll(getParameters());
return new URLParam(map, rawParam);
return doAddParameters(parameters, true);
}
private URLParam doAddParameters(Map<String, String> parameters, boolean skipIfPresent) {
// lazy init, null if no modify
BitSet newKey = null;
BitSet defaultKey = null;
int[] newValueArray = null;
Map<Integer, Integer> newValueMap = null;
Map<String, String> newExtraParams = null;
for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (skipIfPresent && hasParameter(entry.getKey())) {
continue;
}
Integer keyIndex = DynamicParamTable.getKeyIndex(entry.getKey());
if (keyIndex == null) {
// entry key is not present in DynamicParamTable, add it to EXTRA_PARAMS
if (newExtraParams == null) {
newExtraParams = new HashMap<>(EXTRA_PARAMS);
}
newExtraParams.put(entry.getKey(), entry.getValue());
} else {
if (newKey == null) {
newKey = (BitSet) KEY.clone();
}
newKey.set(keyIndex);
if (parameters.size() > ADD_PARAMETER_ON_MOVE_THRESHOLD) {
// recover VALUE back to Map
if (newValueMap == null) {
newValueMap = recoverCompressedValue();
}
newValueMap.put(keyIndex, DynamicParamTable.getValueIndex(entry.getKey(), entry.getValue()));
} else if (!DynamicParamTable.isDefaultValue(entry.getKey(), entry.getValue())) {
// add parameter by moving array
newValueArray = addByMove(VALUE, KEY.get(0, keyIndex).cardinality(), DynamicParamTable.getValueIndex(entry.getKey(), entry.getValue()));
} else {
// value is default value, add to defaultKey directly
if (defaultKey == null) {
defaultKey = (BitSet) DEFAULT_KEY.clone();
}
defaultKey.set(keyIndex);
}
}
}
if (newKey == null) {
newKey = KEY;
}
if (defaultKey == null) {
defaultKey = DEFAULT_KEY;
}
if (newValueArray == null && newValueMap == null) {
newValueArray = VALUE;
}
if (newExtraParams == null) {
newExtraParams = EXTRA_PARAMS;
}
if (newValueMap == null) {
return new URLParam(newKey, defaultKey, newValueArray, newExtraParams, null);
} else {
return new URLParam(newKey, newValueMap, newExtraParams, null);
}
}
private Map<Integer, Integer> recoverCompressedValue() {
Map<Integer, Integer> map = new HashMap<>((int) (KEY.size() / 0.75) + 1);
for (int i = KEY.nextSetBit(0), offset = 0; i >= 0; i = KEY.nextSetBit(i + 1)) {
if (!DEFAULT_KEY.get(i)) {
map.put(i, VALUE[offset++]);
}
}
return map;
}
private int[] addByMove(int[] array, int index, int value) {
if (index < 0 || index > array.length) {
throw new IllegalArgumentException();
}
// copy-on-write
int[] result = new int[array.length + 1];
System.arraycopy(array, 0, result, 0, index);
result[index] = value;
System.arraycopy(array, index, result, index + 1, array.length - index);
return result;
}
/**
* remove specified parameters in URLParam
*
* @param keys keys to being removed
* @return A new URLParam
*/
public URLParam removeParameters(String... keys) {
if (keys == null || keys.length == 0) {
return this;
}
Map<String, String> map = new HashMap<>(getParameters());
// lazy init, null if no modify
BitSet newKey = null;
BitSet defaultKey = null;
int[] newValueArray = null;
Map<String, String> newExtraParams = null;
for (String key : keys) {
map.remove(key);
Integer keyIndex = DynamicParamTable.getKeyIndex(key);
if (keyIndex != null && KEY.get(keyIndex)) {
if (newKey == null) {
newKey = (BitSet) KEY.clone();
}
newKey.clear(keyIndex);
if (DEFAULT_KEY.get(keyIndex)) {
// is default value, remove in DEFAULT_KEY
if (defaultKey == null) {
defaultKey = (BitSet) DEFAULT_KEY.clone();
}
defaultKey.clear(keyIndex);
} else {
// which offset is in VALUE array, set value as -1, compress in the end
if (newValueArray == null) {
newValueArray = new int[VALUE.length];
System.arraycopy(VALUE, 0, newValueArray, 0, VALUE.length);
}
// KEY is immutable
newValueArray[KEY.get(0, keyIndex).cardinality()] = -1;
}
}
if (EXTRA_PARAMS.containsKey(key)) {
if (newExtraParams == null) {
newExtraParams = new HashMap<>(EXTRA_PARAMS);
}
newExtraParams.remove(key);
}
// ignore if key is absent
}
if (map.size() == getParameters().size()) {
return this;
if (newKey == null) {
newKey = KEY;
}
if (defaultKey == null) {
defaultKey = DEFAULT_KEY;
}
if (newValueArray == null) {
newValueArray = VALUE;
} else {
// remove -1 value
newValueArray = compressArray(newValueArray);
}
if (newExtraParams == null) {
newExtraParams = EXTRA_PARAMS;
}
if (newKey.cardinality() + newExtraParams.size() == 0) {
// empty, directly return cache
return EMPTY_PARAM;
} else {
return new URLParam(newKey, defaultKey, newValueArray, newExtraParams, null);
}
return new URLParam(map, rawParam);
}
private int[] compressArray(int[] array) {
int total = 0;
for (int i : array) {
if (i > -1) {
total++;
}
}
if (total == 0) {
return new int[0];
}
int[] result = new int[total];
for (int i = 0, offset = 0; i < array.length; i++) {
// skip if value if less than 0
if (array[i] > -1) {
result[offset++] = array[i];
}
}
return result;
}
/**
* remove all of the parameters in URLParam
*
* @return An empty URLParam
*/
public URLParam clearParameters() {
return new URLParam(new HashMap<>());
return EMPTY_PARAM;
}
/**
* check if specified key is present in URLParam
*
* @param key specified key
* @return present or not
*/
public boolean hasParameter(String key) {
String value = getParameter(key);
return value != null && value.length() > 0;
Integer keyIndex = DynamicParamTable.getKeyIndex(key);
if (keyIndex == null) {
return EXTRA_PARAMS.containsKey(key);
}
return KEY.get(keyIndex);
}
/**
* get value of specified key in URLParam
*
* @param key specified key
* @return value, null if key is absent
*/
public String getParameter(String key) {
return params.get(key);
Integer keyIndex = DynamicParamTable.getKeyIndex(key);
if (keyIndex == null) {
if (EXTRA_PARAMS.containsKey(key)) {
return EXTRA_PARAMS.get(key);
}
return null;
}
if (KEY.get(keyIndex)) {
String value;
if (DEFAULT_KEY.get(keyIndex)) {
value = DynamicParamTable.getDefaultValue(keyIndex);
} else {
int offset = keyIndexToOffset(keyIndex);
value = DynamicParamTable.getValue(keyIndex, offset);
}
if (StringUtils.isEmpty(value)) {
// Forward compatible, make sure key dynamic increment can work.
// In that case, some values which are proceed before increment will set in EXTRA_PARAMS.
return EXTRA_PARAMS.get(key);
} else {
return value;
}
}
return null;
}
private int keyIndexToOffset(int keyIndex) {
int arrayOffset = KEY.get(0, keyIndex).cardinality();
return VALUE[arrayOffset];
}
/**
* get raw string like parameters
*
* @return raw string like parameters
*/
public String getRawParam() {
return rawParam;
if (StringUtils.isNotEmpty(rawParam)) {
return rawParam;
} else {
// empty if parameters have been modified or init by Map
return toString();
}
}
public long getTimestamp() {
@ -217,16 +712,23 @@ public class URLParam implements Serializable {
}
@Override
public int hashCode() {
return Objects.hash(params);
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
URLParam urlParam = (URLParam) o;
return Objects.equals(KEY, urlParam.KEY)
&& Objects.equals(DEFAULT_KEY, urlParam.DEFAULT_KEY)
&& Arrays.equals(VALUE, urlParam.VALUE)
&& Objects.equals(EXTRA_PARAMS, urlParam.EXTRA_PARAMS);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof URLParam)) return false;
URLParam that = (URLParam) obj;
return Objects.equals(this.getParameters(), that.getParameters());
public int hashCode() {
return Objects.hash(KEY, DEFAULT_KEY, Arrays.hashCode(VALUE), EXTRA_PARAMS);
}
@Override
@ -234,38 +736,71 @@ public class URLParam implements Serializable {
if (StringUtils.isNotEmpty(rawParam)) {
return rawParam;
}
if (params == null) {
if ((KEY.cardinality() + EXTRA_PARAMS.size()) == 0) {
return "";
}
StringBuilder buf = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> entry : new TreeMap<>(params).entrySet()) {
if (StringUtils.isNotEmpty(entry.getKey())) {
if (first) {
first = false;
} else {
buf.append("&");
}
buf.append(entry.getKey());
buf.append("=");
buf.append(entry.getValue() == null ? "" : entry.getValue().trim());
}
StringJoiner stringJoiner = new StringJoiner("&");
for (int i = KEY.nextSetBit(0); i >= 0; i = KEY.nextSetBit(i + 1)) {
String key = DynamicParamTable.getKey(i);
String value = DEFAULT_KEY.get(i) ?
DynamicParamTable.getDefaultValue(i) : DynamicParamTable.getValue(i, keyIndexToOffset(i));
value = value == null ? "" : value.trim();
stringJoiner.add(String.format("%s=%s", key, value));
}
return buf.toString();
for (Map.Entry<String, String> entry : EXTRA_PARAMS.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
value = value == null ? "" : value.trim();
stringJoiner.add(String.format("%s=%s", key, value));
}
return stringJoiner.toString();
}
/**
* Parse URLParam
* Init URLParam by constructor is not allowed
* rawParam field in result will be null while {@link URLParam#getRawParam()} will automatically create it
*
* @param params params map added into URLParam
* @return a new URLParam
*/
public static URLParam parse(Map<String, String> params) {
return parse(params, null);
}
/**
* Parse URLParam
* Init URLParam by constructor is not allowed
*
* @param rawParam original rawParam string
* @param encoded if parameters are URL encoded
* @param extraParameters extra parameters to add into URLParam
* @return a new URLParam
*/
public static URLParam parse(String rawParam, boolean encoded, Map<String, String> extraParameters) {
Map<String, String> parameters = URLStrParser.parseParams(rawParam, encoded);
if (CollectionUtils.isNotEmptyMap(extraParameters)) {
parameters.putAll(extraParameters);
}
return new URLParam(parameters, rawParam);
return parse(parameters, rawParam);
}
/**
* Parse URLParam
* Init URLParam by constructor is not allowed
*
* @param rawParam original rawParam string
* @return a new URLParam
*/
public static URLParam parse(String rawParam) {
String[] parts = rawParam.split("&");
Map<String, String> parameters = new UnifiedMap<>((int) (parts.length/.75f) + 1);
BitSet keyBit = new BitSet((int) (parts.length / .75f) + 1);
Map<Integer, Integer> valueMap = new HashMap<>((int) (parts.length / .75f) + 1);
Map<String, String> extraParam = new HashMap<>((int) (parts.length / .75f) + 1);
for (String part : parts) {
part = part.trim();
if (part.length() > 0) {
@ -273,16 +808,69 @@ public class URLParam implements Serializable {
if (j >= 0) {
String key = part.substring(0, j);
String value = part.substring(j + 1);
parameters.put(key, value);
addParameter(keyBit, valueMap, extraParam, key, value, false);
// compatible with lower versions registering "default." keys
if (key.startsWith(DEFAULT_KEY_PREFIX)) {
parameters.putIfAbsent(key.substring(DEFAULT_KEY_PREFIX.length()), value);
addParameter(keyBit, valueMap, extraParam, key.substring(DEFAULT_KEY_PREFIX.length()), value, true);
}
} else {
parameters.put(part, part);
addParameter(keyBit, valueMap, extraParam, part, part, false);
}
}
}
return new URLParam(parameters, rawParam);
return new URLParam(keyBit, valueMap, extraParam, rawParam);
}
/**
* Parse URLParam
* Init URLParam by constructor is not allowed
*
* @param params params map added into URLParam
* @param rawParam original rawParam string, directly add to rawParam field,
* will not effect real key-pairs store in URLParam.
* Please make sure it can correspond with params or will
* cause unexpected result when calling {@link URLParam#getRawParam()}
* and {@link URLParam#toString()} ()}. If you not sure, you can call
* {@link URLParam#parse(String)} to init.
* @return a new URLParam
*/
public static URLParam parse(Map<String, String> params, String rawParam) {
if (CollectionUtils.isNotEmptyMap(params)) {
BitSet keyBit = new BitSet((int) (params.size() / .75f) + 1);
Map<Integer, Integer> valueMap = new HashMap<>((int) (params.size() / .75f) + 1);
Map<String, String> extraParam = new HashMap<>((int) (params.size() / .75f) + 1);
for (Map.Entry<String, String> entry : params.entrySet()) {
addParameter(keyBit, valueMap, extraParam, entry.getKey(), entry.getValue(), false);
}
return new URLParam(keyBit, valueMap, extraParam, rawParam);
} else {
return EMPTY_PARAM;
}
}
private static void addParameter(BitSet keyBit, Map<Integer, Integer> valueMap, Map<String, String> extraParam,
String key, String value, boolean skipIfPresent) {
Integer keyIndex = DynamicParamTable.getKeyIndex(key);
if (skipIfPresent) {
if (keyIndex == null) {
if (extraParam.containsKey(key)) {
return;
}
} else {
if (keyBit.get(keyIndex)) {
return;
}
}
}
if (keyIndex == null) {
extraParam.put(key, value);
} else {
if (!DynamicParamTable.isDefaultValue(key, value)) {
valueMap.put(keyIndex, DynamicParamTable.getValueIndex(key, value));
}
keyBit.set(keyIndex);
}
}
}

View File

@ -0,0 +1,95 @@
/*
* 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.common.url.component.param;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Global Param Cache Table
*/
public final class DynamicParamTable {
private static final List<String> KEYS = new CopyOnWriteArrayList<>();
private static final List<ParamValue> VALUES = new CopyOnWriteArrayList<>();
private static final Map<String, Integer> KEY2INDEX = new HashMap<>(64);
private DynamicParamTable() {
throw new IllegalStateException();
}
static {
init();
}
public static Integer getKeyIndex(String key) {
return KEY2INDEX.get(key);
}
public static int getValueIndex(String key, String value) {
Integer idx = getKeyIndex(key);
if (idx == null) {
throw new IllegalArgumentException("Cannot found key in url param:" + key);
}
ParamValue paramValue = VALUES.get(idx);
return paramValue.getIndex(value);
}
public static String getKey(int offset) {
return KEYS.get(offset);
}
public static boolean isDefaultValue(String key, String value) {
return Objects.equals(value, VALUES.get(getKeyIndex(key)).defaultVal());
}
public static String getValue(int vi, int offset) {
return VALUES.get(vi).getN(offset);
}
public static String getDefaultValue(int vi) {
return VALUES.get(vi).defaultVal();
}
public static void init() {
List<String> keys = new LinkedList<>();
List<ParamValue> values = new LinkedList<>();
Map<String, Integer> key2Index = new HashMap<>(64);
keys.add("");
values.add(new DynamicValues(null));
// Cache key and defaultValue
keys.add("version");
values.add(new DynamicValues(null));
keys.add("side");
values.add(new FixedParamValue("consumer","provider"));
for (int i = 0; i < keys.size(); i++) {
if (!keys.get(i).isEmpty()) {
key2Index.put(keys.get(i), i);
}
}
KEYS.addAll(keys);
VALUES.addAll(values);
KEY2INDEX.putAll(key2Index);
}
}

View File

@ -0,0 +1,70 @@
/*
* 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.common.url.component.param;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class DynamicValues implements ParamValue {
private final Map<Integer, String> index2Value = new ConcurrentHashMap<>();
private final Map<String, Integer> value2Index = new ConcurrentHashMap<>();
private int indexSeq = 0;
public DynamicValues(String defaultVal) {
if (defaultVal == null) {
indexSeq += 1;
} else {
add(defaultVal);
}
}
public int add(String value) {
Integer index = value2Index.get(value);
if (index != null) {
return index;
} else {
synchronized (this) {
// thread safe
if (!value2Index.containsKey(value)) {
value2Index.put(value, indexSeq);
index2Value.put(indexSeq, value);
indexSeq += 1;
}
}
}
return value2Index.get(value);
}
@Override
public String getN(int n) {
return index2Value.get(n);
}
@Override
public int getIndex(String value) {
Integer index = value2Index.get(value);
if (index == null) {
return add(value);
}
return index;
}
@Override
public String defaultVal() {
return getN(0);
}
}

View File

@ -0,0 +1,69 @@
/*
* 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.common.url.component.param;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* In lower case
*/
public class FixedParamValue implements ParamValue {
private final String[] values;
private final Map<String, Integer> val2Index;
public FixedParamValue(String... values) {
if (values.length == 0) {
throw new IllegalArgumentException("the array size of values should be larger than 0");
}
this.values = values;
Map<String, Integer> valueMap = new HashMap<>(values.length);
for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
valueMap.put(values[i].toLowerCase(Locale.ROOT), i);
}
}
val2Index = Collections.unmodifiableMap(valueMap);
}
/**
* DEFAULT value will be returned if n = 0
*/
@Override
public String getN(int n) {
return values[n];
}
@Override
public int getIndex(String value) {
Integer offset = val2Index.get(value.toLowerCase(Locale.ROOT));
if (offset == null) {
throw new IllegalArgumentException("unrecognized value " + value
+ " , please check if value is illegal. " +
"Permitted values: " + Arrays.asList(values));
}
return offset;
}
@Override
public String defaultVal() {
return values[0];
}
}

View File

@ -0,0 +1,32 @@
/*
* 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.common.url.component.param;
import java.util.HashSet;
public class IgnoredParam {
private static final HashSet<String> IGNORED = new HashSet<>();
static {
IGNORED.add("timestamp");
}
static boolean ignore(String key) {
return IGNORED.contains(key);
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.common.url.component.param;
public interface ParamValue {
/**
* get value at the specified index.
*
* @param n the nth value
* @return the value stored at index = n
*/
String getN(int n);
/**
* max index is 2^31 - 1
*
* @param value the stored value
* @return the index of value
*/
int getIndex(String value);
/**
* get default value
*
* @return the default value stored at index = 0
*/
String defaultVal();
}

View File

@ -0,0 +1,261 @@
/*
* 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.common.url;
import org.apache.dubbo.common.url.component.URLParam;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class URLParamTest {
@Test
public void testParseWithRawParam() {
URLParam urlParam1 = URLParam.parse("aaa=aaa&bbb&version=1.0&default.ccc=123");
Assertions.assertEquals("aaa", urlParam1.getParameter("aaa"));
Assertions.assertEquals("bbb", urlParam1.getParameter("bbb"));
Assertions.assertEquals("1.0", urlParam1.getParameter("version"));
Assertions.assertEquals("123", urlParam1.getParameter("default.ccc"));
Assertions.assertEquals("123", urlParam1.getParameter("ccc"));
Assertions.assertEquals(urlParam1, URLParam.parse(urlParam1.getRawParam()));
Assertions.assertEquals(urlParam1, URLParam.parse(urlParam1.toString()));
URLParam urlParam2 = URLParam.parse("aaa%3dtest", true, null);
Assertions.assertEquals("test", urlParam2.getParameter("aaa"));
Map<String, String> overrideMap = Collections.singletonMap("aaa", "bbb");
URLParam urlParam3 = URLParam.parse("aaa%3dtest", true, overrideMap);
Assertions.assertEquals("bbb", urlParam3.getParameter("aaa"));
URLParam urlParam4 = URLParam.parse("ccc=456&&default.ccc=123");
Assertions.assertEquals("456", urlParam4.getParameter("ccc"));
URLParam urlParam5 = URLParam.parse("version=2.0&&default.version=1.0");
Assertions.assertEquals("2.0", urlParam5.getParameter("version"));
}
@Test
public void testParseWithMap() {
Map<String, String> map = new HashMap<>();
map.put("aaa", "aaa");
map.put("bbb", "bbb");
map.put("version", "2.0");
map.put("side", "consumer");
URLParam urlParam1 = URLParam.parse(map);
Assertions.assertEquals("aaa", urlParam1.getParameter("aaa"));
Assertions.assertEquals("bbb", urlParam1.getParameter("bbb"));
Assertions.assertEquals("2.0", urlParam1.getParameter("version"));
Assertions.assertEquals("consumer", urlParam1.getParameter("side"));
Assertions.assertEquals(urlParam1, URLParam.parse(urlParam1.getRawParam()));
map.put("bbb", "ccc");
Assertions.assertEquals("bbb", urlParam1.getParameter("bbb"));
URLParam urlParam2 = URLParam.parse(map);
Assertions.assertEquals("ccc", urlParam2.getParameter("bbb"));
URLParam urlParam3 = URLParam.parse(null, null);
Assertions.assertFalse(urlParam3.hasParameter("aaa"));
Assertions.assertEquals(urlParam3, URLParam.parse(urlParam3.getRawParam()));
}
@Test
public void testGetParameter() {
URLParam urlParam1 = URLParam.parse("aaa=aaa&bbb&version=1.0&default.ccc=123");
Assertions.assertNull(urlParam1.getParameter("abcde"));
URLParam urlParam2 = URLParam.parse("aaa=aaa&bbb&default.ccc=123");
Assertions.assertNull(urlParam2.getParameter("version"));
URLParam urlParam3 = URLParam.parse("aaa=aaa&side=consumer");
Assertions.assertEquals("consumer", urlParam3.getParameter("side"));
URLParam urlParam4 = URLParam.parse("aaa=aaa&side=provider");
Assertions.assertEquals("provider", urlParam4.getParameter("side"));
}
@Test
public void testHasParameter() {
URLParam urlParam1 = URLParam.parse("aaa=aaa&side=provider");
Assertions.assertTrue(urlParam1.hasParameter("aaa"));
Assertions.assertFalse(urlParam1.hasParameter("bbb"));
Assertions.assertTrue(urlParam1.hasParameter("side"));
Assertions.assertFalse(urlParam1.hasParameter("version"));
}
@Test
public void testRemoveParameters() {
URLParam urlParam1 = URLParam.parse("aaa=aaa&side=provider&version=1.0");
Assertions.assertTrue(urlParam1.hasParameter("aaa"));
Assertions.assertTrue(urlParam1.hasParameter("side"));
Assertions.assertTrue(urlParam1.hasParameter("version"));
URLParam urlParam2 = urlParam1.removeParameters("side");
Assertions.assertFalse(urlParam2.hasParameter("side"));
URLParam urlParam3 = urlParam1.removeParameters("aaa", "version");
Assertions.assertFalse(urlParam3.hasParameter("aaa"));
Assertions.assertFalse(urlParam3.hasParameter("version"));
URLParam urlParam4 = urlParam1.removeParameters();
Assertions.assertTrue(urlParam4.hasParameter("aaa"));
Assertions.assertTrue(urlParam4.hasParameter("side"));
Assertions.assertTrue(urlParam4.hasParameter("version"));
URLParam urlParam5 = urlParam1.clearParameters();
Assertions.assertFalse(urlParam5.hasParameter("aaa"));
Assertions.assertFalse(urlParam5.hasParameter("side"));
Assertions.assertFalse(urlParam5.hasParameter("version"));
URLParam urlParam6 = urlParam1.removeParameters("aaa");
Assertions.assertFalse(urlParam6.hasParameter("aaa"));
URLParam urlParam7 = URLParam.parse("side=consumer").removeParameters("side");
Assertions.assertFalse(urlParam7.hasParameter("side"));
}
@Test
public void testAddParameters() {
URLParam urlParam1 = URLParam.parse("aaa=aaa&side=provider");
Assertions.assertTrue(urlParam1.hasParameter("aaa"));
Assertions.assertTrue(urlParam1.hasParameter("side"));
URLParam urlParam2 = urlParam1.addParameter("bbb", "bbb");
Assertions.assertEquals("aaa", urlParam2.getParameter("aaa"));
Assertions.assertEquals("bbb", urlParam2.getParameter("bbb"));
URLParam urlParam3 = urlParam1.addParameter("aaa", "ccc");
Assertions.assertEquals("aaa", urlParam1.getParameter("aaa"));
Assertions.assertEquals("ccc", urlParam3.getParameter("aaa"));
URLParam urlParam4 = urlParam1.addParameter("aaa", "aaa");
Assertions.assertEquals("aaa", urlParam4.getParameter("aaa"));
URLParam urlParam5 = urlParam1.addParameter("version", "0.1");
Assertions.assertEquals("0.1", urlParam5.getParameter("version"));
URLParam urlParam6 = urlParam5.addParameterIfAbsent("version", "0.2");
Assertions.assertEquals("0.1", urlParam6.getParameter("version"));
URLParam urlParam7 = urlParam1.addParameterIfAbsent("version", "0.2");
Assertions.assertEquals("0.2", urlParam7.getParameter("version"));
Map<String, String> map = new HashMap<>();
map.put("version", "1.0");
map.put("side", "provider");
URLParam urlParam8 = urlParam1.addParameters(map);
Assertions.assertEquals("1.0", urlParam8.getParameter("version"));
Assertions.assertEquals("provider", urlParam8.getParameter("side"));
map.put("side", "consumer");
Assertions.assertEquals("provider", urlParam8.getParameter("side"));
URLParam urlParam9 = urlParam8.addParameters(map);
Assertions.assertEquals("consumer", urlParam9.getParameter("side"));
URLParam urlParam10 = urlParam8.addParametersIfAbsent(map);
Assertions.assertEquals("provider", urlParam10.getParameter("side"));
Assertions.assertThrows(IllegalArgumentException.class,
() -> urlParam1.addParameter("side", "unrecognized"));
}
@Test
public void testURLParamMap() {
URLParam urlParam1 = URLParam.parse("");
Assertions.assertTrue(urlParam1.getParameters().isEmpty());
Assertions.assertEquals(0, urlParam1.getParameters().size());
Assertions.assertFalse(urlParam1.getParameters().containsKey("aaa"));
Assertions.assertFalse(urlParam1.getParameters().containsKey("version"));
Assertions.assertFalse(urlParam1.getParameters().containsKey(new Object()));
URLParam urlParam2 = URLParam.parse("aaa=aaa&version=1.0");
URLParam.URLParamMap urlParam2Map = (URLParam.URLParamMap) urlParam2.getParameters();
Assertions.assertTrue(urlParam2Map.containsKey("version"));
Assertions.assertFalse(urlParam2Map.containsKey("side"));
Assertions.assertTrue(urlParam2Map.containsValue("1.0"));
Assertions.assertFalse(urlParam2Map.containsValue("2.0"));
Assertions.assertEquals("1.0", urlParam2Map.get("version"));
Assertions.assertEquals("aaa", urlParam2Map.get("aaa"));
Assertions.assertNull(urlParam2Map.get(new Object()));
Assertions.assertEquals(urlParam2, urlParam2Map.getUrlParam());
urlParam2Map.put("version", "1.0");
Assertions.assertEquals(urlParam2, urlParam2Map.getUrlParam());
urlParam2Map.putAll(Collections.singletonMap("version", "1.0"));
Assertions.assertEquals(urlParam2, urlParam2Map.getUrlParam());
urlParam2Map.put("side", "consumer");
Assertions.assertNotEquals(urlParam2, urlParam2Map.getUrlParam());
urlParam2Map = (URLParam.URLParamMap) urlParam2.getParameters();
Assertions.assertEquals(urlParam2, urlParam2Map.getUrlParam());
urlParam2Map.remove("version");
Assertions.assertNotEquals(urlParam2, urlParam2Map.getUrlParam());
Assertions.assertFalse(urlParam2Map.containsValue("version"));
Assertions.assertNull(urlParam2Map.getUrlParam().getParameter("version"));
urlParam2Map = (URLParam.URLParamMap) urlParam2.getParameters();
Assertions.assertEquals(urlParam2, urlParam2Map.getUrlParam());
urlParam2Map.clear();
Assertions.assertTrue(urlParam2Map.isEmpty());
Assertions.assertEquals(0, urlParam2Map.size());
Assertions.assertNull(urlParam2Map.getUrlParam().getParameter("aaa"));
Assertions.assertNull(urlParam2Map.getUrlParam().getParameter("version"));
urlParam2Map = (URLParam.URLParamMap) urlParam2.getParameters();
Assertions.assertEquals(urlParam2, urlParam2Map.getUrlParam());
URLParam urlParam3 = URLParam.parse("aaa=aaa&version=1.0");
Assertions.assertTrue(CollectionUtils.mapEquals(urlParam2Map, urlParam3.getParameters()));
Assertions.assertTrue(CollectionUtils.equals(urlParam2Map.entrySet(), urlParam3.getParameters().entrySet()));
Assertions.assertTrue(CollectionUtils.equals(urlParam2Map.keySet(), urlParam3.getParameters().keySet()));
Assertions.assertTrue(CollectionUtils.equals(urlParam2Map.values(), urlParam3.getParameters().values()));
URLParam urlParam4 = URLParam.parse("aaa=aaa&version=1.0&side=consumer");
Assertions.assertFalse(CollectionUtils.mapEquals(urlParam2Map, urlParam4.getParameters()));
Assertions.assertFalse(CollectionUtils.equals(urlParam2Map.entrySet(), urlParam4.getParameters().entrySet()));
Assertions.assertFalse(CollectionUtils.equals(urlParam2Map.keySet(), urlParam4.getParameters().keySet()));
Assertions.assertFalse(CollectionUtils.equals(urlParam2Map.values(), urlParam4.getParameters().values()));
Set<Map<String,String>> set = new HashSet<>();
set.add(urlParam2Map);
set.add(urlParam3.getParameters());
Assertions.assertEquals(1,set.size());
set.add(urlParam4.getParameters());
Assertions.assertEquals(2,set.size());
}
}