This commit is contained in:
Looly
2022-11-30 23:51:36 +08:00
parent 185cd09ab0
commit 75d536839c
34 changed files with 877 additions and 2145 deletions

View File

@@ -21,12 +21,14 @@ import java.util.stream.Collectors;
* <p>当通过实例方法获得值集合时,若该集合允许修改,则对值集合的修改将会影响到其所属的{@link MultiValueMap}实例,反之亦然。
* 因此当同时遍历当前实例或者值集合时,若存在写操作,则需要注意可能引发的{@link ConcurrentModificationException}。
*
* @param <K> 键类型
* @param <V> 值类型
* @author huangchengxing
* @since 6.0.0
* @see AbsCollValueMap
* @see CollectionValueMap
* @see ListValueMap
* @see SetValueMap
* @since 6.0.0
*/
public interface MultiValueMap<K, V> extends Map<K, Collection<V>> {
@@ -64,7 +66,7 @@ public interface MultiValueMap<K, V> extends Map<K, Collection<V>> {
* Collection<V> coll = entry.getValues();
* for (V val : coll) {
* map.putValue(key, val)
* }
* }
* }
* }</pre>
*
@@ -81,7 +83,7 @@ public interface MultiValueMap<K, V> extends Map<K, Collection<V>> {
* <pre>{@code
* for (V val : coll) {
* map.putValue(key, val)
* }
* }
* }</pre>
*
* @param key 键
@@ -95,7 +97,7 @@ public interface MultiValueMap<K, V> extends Map<K, Collection<V>> {
* <pre>{@code
* for (V val : values) {
* map.putValue(key, val)
* }
* }
* }</pre>
*
* @param key 键
@@ -137,7 +139,7 @@ public interface MultiValueMap<K, V> extends Map<K, Collection<V>> {
/**
* 将一批值从指定键下的值集合中删除
*
* @param key 键
* @param key
* @param values 值数组
* @return 是否成功删除
*/
@@ -149,7 +151,7 @@ public interface MultiValueMap<K, V> extends Map<K, Collection<V>> {
/**
* 将一批值从指定键下的值集合中删除
*
* @param key 键
* @param key
* @param values 值集合
* @return 是否成功删除
*/
@@ -236,7 +238,7 @@ public interface MultiValueMap<K, V> extends Map<K, Collection<V>> {
* Collection<V> coll = entry.getValues();
* for (V val : coll) {
* consumer.accept(key, val);
* }
* }
* }
* }</pre>
*
@@ -259,8 +261,8 @@ public interface MultiValueMap<K, V> extends Map<K, Collection<V>> {
*/
default Collection<V> allValues() {
return values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
}

View File

@@ -558,7 +558,6 @@ public class URLUtil {
if (StrUtil.isNotEmpty(body)) {
// 去除开头的\或者/
//noinspection ConstantConditions
body = body.replaceAll("^[\\\\/]+", StrUtil.EMPTY);
// 替换\为/
body = body.replace("\\", "/");

View File

@@ -0,0 +1,215 @@
package cn.hutool.core.net.url;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.CharsetUtil;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class UrlQueryUtil {
/**
* 将Map形式的Form表单数据转换为Url参数形式会自动url编码键和值
*
* @param paramMap 表单数据
* @return url参数
*/
public static String toQuery(final Map<String, ?> paramMap) {
return toQuery(paramMap, CharsetUtil.UTF_8);
}
/**
* 将Map形式的Form表单数据转换为Url参数形式<br>
* paramMap中如果key为空null和""会被忽略如果value为null会被做为空白符""<br>
* 会自动url编码键和值<br>
* 此方法用于拼接URL中的Query部分并不适用于POST请求中的表单
*
* <pre>
* key1=v1&amp;key2=&amp;key3=v3
* </pre>
*
* @param paramMap 表单数据
* @param charset 编码,{@code null} 表示不encode键值对
* @return url参数
* @see #toQuery(Map, Charset, boolean)
*/
public static String toQuery(final Map<String, ?> paramMap, final Charset charset) {
return toQuery(paramMap, charset, false);
}
/**
* 将Map形式的Form表单数据转换为Url参数形式<br>
* paramMap中如果key为空null和""会被忽略如果value为null会被做为空白符""<br>
* 会自动url编码键和值
*
* <pre>
* key1=v1&amp;key2=&amp;key3=v3
* </pre>
*
* @param paramMap 表单数据
* @param charset 编码null表示不encode键值对
* @param isFormUrlEncoded 是否为x-www-form-urlencoded模式此模式下空格会编码为'+'
* @return url参数
* @since 5.7.16
*/
public static String toQuery(final Map<String, ?> paramMap, final Charset charset, final boolean isFormUrlEncoded) {
return UrlQuery.of(paramMap, isFormUrlEncoded).build(charset);
}
/**
* 对URL参数做编码只编码键和值<br>
* 提供的值可以是url附带参数但是不能只是url
*
* <p>注意此方法只能标准化整个URL并不适合于单独编码参数值</p>
*
* @param urlWithParams url和参数可以包含url本身也可以单独参数
* @param charset 编码
* @return 编码后的url和参数
* @since 4.0.1
*/
public static String encodeQuery(final String urlWithParams, final Charset charset) {
if (StrUtil.isBlank(urlWithParams)) {
return StrUtil.EMPTY;
}
String urlPart = null; // url部分不包括问号
String paramPart; // 参数部分
final int pathEndPos = urlWithParams.indexOf('?');
if (pathEndPos > -1) {
// url + 参数
urlPart = StrUtil.subPre(urlWithParams, pathEndPos);
paramPart = StrUtil.subSuf(urlWithParams, pathEndPos + 1);
if (StrUtil.isBlank(paramPart)) {
// 无参数返回url
return urlPart;
}
} else if (false == StrUtil.contains(urlWithParams, '=')) {
// 无参数的URL
return urlWithParams;
} else {
// 无URL的参数
paramPart = urlWithParams;
}
paramPart = normalizeQuery(paramPart, charset);
return StrUtil.isBlank(urlPart) ? paramPart : urlPart + "?" + paramPart;
}
/**
* 标准化参数字符串即URL中后的部分
*
* <p>注意此方法只能标准化整个URL并不适合于单独编码参数值</p>
*
* @param queryPart 参数字符串
* @param charset 编码
* @return 标准化的参数字符串
* @since 4.5.2
*/
public static String normalizeQuery(final String queryPart, final Charset charset) {
if (StrUtil.isEmpty(queryPart)) {
return queryPart;
}
final StringBuilder builder = new StringBuilder(queryPart.length() + 16);
final int len = queryPart.length();
String name = null;
int pos = 0; // 未处理字符开始位置
char c; // 当前字符
int i; // 当前字符位置
for (i = 0; i < len; i++) {
c = queryPart.charAt(i);
if (c == '=') { // 键值对的分界点
if (null == name) {
// 只有=前未定义name时被当作键值分界符否则做为普通字符
name = (pos == i) ? StrUtil.EMPTY : queryPart.substring(pos, i);
pos = i + 1;
}
} else if (c == '&') { // 参数对的分界点
if (pos != i) {
if (null == name) {
// 对于像&a&这类无参数值的字符串我们将name为a的值设为""
name = queryPart.substring(pos, i);
builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=');
} else {
builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=')
.append(RFC3986.QUERY_PARAM_VALUE.encode(queryPart.substring(pos, i), charset)).append('&');
}
name = null;
}
pos = i + 1;
}
}
// 结尾处理
if (null != name) {
builder.append(URLEncoder.encodeQuery(name, charset)).append('=');
}
if (pos != i) {
if (null == name && pos > 0) {
builder.append('=');
}
builder.append(URLEncoder.encodeQuery(queryPart.substring(pos, i), charset));
}
// 以&结尾则去除之
final int lastIndex = builder.length() - 1;
if ('&' == builder.charAt(lastIndex)) {
builder.delete(lastIndex, builder.length());
}
return builder.toString();
}
/**
* 将URL参数解析为Map也可以解析Post中的键值对参数
*
* @param paramsStr 参数字符串或者带参数的Path
* @param charset 字符集
* @return 参数Map
* @since 5.2.6
*/
public static Map<String, String> decodeQuery(final String paramsStr, final Charset charset) {
final Map<CharSequence, CharSequence> queryMap = UrlQuery.of(paramsStr, charset).getQueryMap();
if (MapUtil.isEmpty(queryMap)) {
return MapUtil.empty();
}
return Convert.toMap(String.class, String.class, queryMap);
}
/**
* 将URL参数解析为Map也可以解析Post中的键值对参数
*
* @param paramsStr 参数字符串或者带参数的Path
* @param charset 字符集
* @return 参数Map
*/
public static Map<String, List<String>> decodeQuery(final String paramsStr, final String charset) {
return decodeQueryList(paramsStr, CharsetUtil.charset(charset));
}
/**
* 将URL参数解析为Map也可以解析Post中的键值对参数
*
* @param paramsStr 参数字符串或者带参数的Path
* @param charset 字符集
* @return 参数Map
* @since 5.2.6
*/
public static Map<String, List<String>> decodeQueryList(final String paramsStr, final Charset charset) {
final Map<CharSequence, CharSequence> queryMap = UrlQuery.of(paramsStr, charset).getQueryMap();
if (MapUtil.isEmpty(queryMap)) {
return MapUtil.empty();
}
final Map<String, List<String>> params = new LinkedHashMap<>();
queryMap.forEach((key, value) -> {
final List<String> values = params.computeIfAbsent(StrUtil.str(key), k -> new ArrayList<>(1));
// 一般是一个参数
values.add(StrUtil.str(value));
});
return params;
}
}

View File

@@ -0,0 +1,153 @@
package cn.hutool.core.net.url;
import cn.hutool.core.util.CharsetUtil;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import java.util.Map;
public class UrlQueryUtilTest {
@Test
public void decodeQueryTest() {
final String paramsStr = "uuuu=0&a=b&c=%3F%23%40!%24%25%5E%26%3Ddsssss555555";
final Map<String, List<String>> map = UrlQueryUtil.decodeQuery(paramsStr, CharsetUtil.NAME_UTF_8);
Assert.assertEquals("0", map.get("uuuu").get(0));
Assert.assertEquals("b", map.get("a").get(0));
Assert.assertEquals("?#@!$%^&=dsssss555555", map.get("c").get(0));
}
@Test
public void decodeQueryTest2() {
// 参数值存在分界标记等号时
final Map<String, String> paramMap = UrlQueryUtil.decodeQuery("https://www.xxx.com/api.action?aa=123&f_token=NzBkMjQxNDM1MDVlMDliZTk1OTU3ZDI1OTI0NTBiOWQ=", CharsetUtil.UTF_8);
Assert.assertEquals("123",paramMap.get("aa"));
Assert.assertEquals("NzBkMjQxNDM1MDVlMDliZTk1OTU3ZDI1OTI0NTBiOWQ=",paramMap.get("f_token"));
}
@Test
public void toQueryTest() {
final String paramsStr = "uuuu=0&a=b&c=3Ddsssss555555";
final Map<String, List<String>> map = UrlQueryUtil.decodeQuery(paramsStr, CharsetUtil.NAME_UTF_8);
final String encodedParams = UrlQueryUtil.toQuery(map);
Assert.assertEquals(paramsStr, encodedParams);
}
@Test
public void encodeParamTest() {
// ?单独存在去除之,&单位位于末尾去除之
String paramsStr = "?a=b&c=d&";
String encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals("a=b&c=d", encode);
// url不参与转码
paramsStr = "http://www.abc.dd?a=b&c=d&";
encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals("http://www.abc.dd?a=b&c=d", encode);
// b=b中的=被当作值的一部分不做encode
paramsStr = "a=b=b&c=d&";
encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals("a=b=b&c=d", encode);
// =d的情况被处理为key为空
paramsStr = "a=bbb&c=d&=d";
encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals("a=bbb&c=d&=d", encode);
// d=的情况被处理为value为空
paramsStr = "a=bbb&c=d&d=";
encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals("a=bbb&c=d&d=", encode);
// 多个&&被处理为单个,相当于空条件
paramsStr = "a=bbb&c=d&&&d=";
encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals("a=bbb&c=d&d=", encode);
// &d&相当于只有键,无值得情况
paramsStr = "a=bbb&c=d&d&";
encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals("a=bbb&c=d&d=", encode);
// 中文的键和值被编码
paramsStr = "a=bbb&c=你好&哈喽&";
encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals("a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD=", encode);
// URL原样输出
paramsStr = "https://www.hutool.cn/";
encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals(paramsStr, encode);
// URL原样输出
paramsStr = "https://www.hutool.cn/?";
encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8);
Assert.assertEquals("https://www.hutool.cn/", encode);
}
@Test
public void decodeParamTest() {
// 开头的?被去除
String a = "?a=b&c=d&";
Map<String, List<String>> map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8);
Assert.assertEquals("b", map.get("a").get(0));
Assert.assertEquals("d", map.get("c").get(0));
// =e被当作空为keye为value
a = "?a=b&c=d&=e";
map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8);
Assert.assertEquals("b", map.get("a").get(0));
Assert.assertEquals("d", map.get("c").get(0));
Assert.assertEquals("e", map.get("").get(0));
// 多余的&去除
a = "?a=b&c=d&=e&&&&";
map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8);
Assert.assertEquals("b", map.get("a").get(0));
Assert.assertEquals("d", map.get("c").get(0));
Assert.assertEquals("e", map.get("").get(0));
// 值为空
a = "?a=b&c=d&e=";
map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8);
Assert.assertEquals("b", map.get("a").get(0));
Assert.assertEquals("d", map.get("c").get(0));
Assert.assertEquals("", map.get("e").get(0));
// &=被作为键和值都为空
a = "a=b&c=d&=";
map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8);
Assert.assertEquals("b", map.get("a").get(0));
Assert.assertEquals("d", map.get("c").get(0));
Assert.assertEquals("", map.get("").get(0));
// &e&这类单独的字符串被当作key
a = "a=b&c=d&e&";
map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8);
Assert.assertEquals("b", map.get("a").get(0));
Assert.assertEquals("d", map.get("c").get(0));
Assert.assertNull(map.get("e").get(0));
Assert.assertNull(map.get("").get(0));
// 被编码的键和值被还原
a = "a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD=";
map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8);
Assert.assertEquals("bbb", map.get("a").get(0));
Assert.assertEquals("你好", map.get("c").get(0));
Assert.assertEquals("", map.get("哈喽").get(0));
}
@Test
public void normalizeQueryTest() {
final String encodeResult = UrlQueryUtil.normalizeQuery("参数", CharsetUtil.UTF_8);
Assert.assertEquals("%E5%8F%82%E6%95%B0", encodeResult);
}
@Test
public void normalizeBlankQueryTest() {
final String encodeResult = UrlQueryUtil.normalizeQuery("", CharsetUtil.UTF_8);
Assert.assertEquals("", encodeResult);
}
}