From b9bca6a089cc24fbecd49e03f820f9f7e1e8eb82 Mon Sep 17 00:00:00 2001 From: Albumen Kevin Date: Sun, 25 Apr 2021 15:59:22 +0800 Subject: [PATCH] [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 --- .../java/org/apache/dubbo/common/URL.java | 4 +- .../url/component/ServiceConfigURL.java | 4 +- .../dubbo/common/url/component/URLParam.java | 740 ++++++++++++++++-- .../component/param/DynamicParamTable.java | 95 +++ .../url/component/param/DynamicValues.java | 70 ++ .../url/component/param/FixedParamValue.java | 69 ++ .../url/component/param/IgnoredParam.java | 32 + .../url/component/param/ParamValue.java | 43 + .../apache/dubbo/common/url/URLParamTest.java | 261 ++++++ 9 files changed, 1238 insertions(+), 80 deletions(-) create mode 100644 dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/DynamicParamTable.java create mode 100644 dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/DynamicValues.java create mode 100644 dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/FixedParamValue.java create mode 100644 dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/IgnoredParam.java create mode 100644 dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/ParamValue.java create mode 100644 dubbo-common/src/test/java/org/apache/dubbo/common/url/URLParamTest.java diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/URL.java b/dubbo-common/src/main/java/org/apache/dubbo/common/URL.java index 485ce7c91f..996db17f4f 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/URL.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/URL.java @@ -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) { diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/ServiceConfigURL.java b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/ServiceConfigURL.java index 2bbb0620dd..8702f5ccb8 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/ServiceConfigURL.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/ServiceConfigURL.java @@ -45,7 +45,7 @@ public class ServiceConfigURL extends URL { int port, String path, Map 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 parameters, Map 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 newURL(URLAddress urlAddress, URLParam urlParam) { diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/URLParam.java b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/URLParam.java index cea92cb396..3d1054b228 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/URLParam.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/URLParam.java @@ -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} + *
+ * Using {@link DynamicParamTable} to compress common keys (i.e. side, version) + *
+ * {@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. + *
+ * 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 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 EXTRA_PARAMS; //cache private transient Map> methodParameters; private transient long timestamp; - public URLParam(Map 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 params, String rawParam) { - this.params = Collections.unmodifiableMap((params == null ? new HashMap<>() : new HashMap<>(params))); + private URLParam(BitSet key, Map value, Map 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 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> 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 + *
+ * 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 { + private URLParam urlParam; + + public URLParamMap(URLParam urlParam) { + this.urlParam = urlParam; + } + + public static class Node implements Map.Entry { + 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 m) { + urlParam = urlParam.addParameters((Map) m); + } + + @Override + public void clear() { + urlParam = urlParam.clearParameters(); + } + + @Override + public Set keySet() { + Set 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 entry : urlParam.EXTRA_PARAMS.entrySet()) { + set.add(entry.getKey()); + } + return Collections.unmodifiableSet(set); + } + + @Override + public Collection values() { + Set 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 entry : urlParam.EXTRA_PARAMS.entrySet()) { + set.add(entry.getValue()); + } + return Collections.unmodifiableSet(set); + } + + @Override + public Set> entrySet() { + Set> 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 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 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 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 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 parameters) { if (CollectionUtils.isEmptyMap(parameters)) { @@ -159,53 +436,271 @@ public class URLParam implements Serializable { return this; } - Map 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 parameters) { if (CollectionUtils.isEmptyMap(parameters)) { return this; } - Map 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 parameters, boolean skipIfPresent) { + // lazy init, null if no modify + BitSet newKey = null; + BitSet defaultKey = null; + int[] newValueArray = null; + Map newValueMap = null; + Map newExtraParams = null; + for (Map.Entry 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 recoverCompressedValue() { + Map 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 map = new HashMap<>(getParameters()); + // lazy init, null if no modify + BitSet newKey = null; + BitSet defaultKey = null; + int[] newValueArray = null; + Map 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 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 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 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 extraParameters) { Map 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 parameters = new UnifiedMap<>((int) (parts.length/.75f) + 1); + + BitSet keyBit = new BitSet((int) (parts.length / .75f) + 1); + Map valueMap = new HashMap<>((int) (parts.length / .75f) + 1); + Map 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 params, String rawParam) { + if (CollectionUtils.isNotEmptyMap(params)) { + BitSet keyBit = new BitSet((int) (params.size() / .75f) + 1); + Map valueMap = new HashMap<>((int) (params.size() / .75f) + 1); + Map extraParam = new HashMap<>((int) (params.size() / .75f) + 1); + + for (Map.Entry 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 valueMap, Map 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); + } } } diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/DynamicParamTable.java b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/DynamicParamTable.java new file mode 100644 index 0000000000..5ecac4ee3e --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/DynamicParamTable.java @@ -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 KEYS = new CopyOnWriteArrayList<>(); + private static final List VALUES = new CopyOnWriteArrayList<>(); + private static final Map 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 keys = new LinkedList<>(); + List values = new LinkedList<>(); + Map 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); + } +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/DynamicValues.java b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/DynamicValues.java new file mode 100644 index 0000000000..69f4b33bf0 --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/DynamicValues.java @@ -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 index2Value = new ConcurrentHashMap<>(); + private final Map 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); + } +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/FixedParamValue.java b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/FixedParamValue.java new file mode 100644 index 0000000000..a54cfee83a --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/FixedParamValue.java @@ -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 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 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]; + } +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/IgnoredParam.java b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/IgnoredParam.java new file mode 100644 index 0000000000..cfc768f3b3 --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/IgnoredParam.java @@ -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 IGNORED = new HashSet<>(); + + static { + IGNORED.add("timestamp"); + } + + static boolean ignore(String key) { + return IGNORED.contains(key); + } + +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/ParamValue.java b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/ParamValue.java new file mode 100644 index 0000000000..81ea16845c --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/url/component/param/ParamValue.java @@ -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(); +} diff --git a/dubbo-common/src/test/java/org/apache/dubbo/common/url/URLParamTest.java b/dubbo-common/src/test/java/org/apache/dubbo/common/url/URLParamTest.java new file mode 100644 index 0000000000..7630e6d0f5 --- /dev/null +++ b/dubbo-common/src/test/java/org/apache/dubbo/common/url/URLParamTest.java @@ -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 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 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 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> set = new HashSet<>(); + + set.add(urlParam2Map); + set.add(urlParam3.getParameters()); + Assertions.assertEquals(1,set.size()); + + set.add(urlParam4.getParameters()); + Assertions.assertEquals(2,set.size()); + } +}