mirror of
https://gitee.com/dromara/hutool.git
synced 2025-04-05 17:37:59 +08:00
add class
This commit is contained in:
parent
2818809ee8
commit
065c6d61c8
@ -62,8 +62,11 @@ public class PercentCodec implements Serializable {
|
||||
* 存放安全编码
|
||||
*/
|
||||
private final BitSet safeCharacters;
|
||||
|
||||
/**
|
||||
* 是否编码空格为+
|
||||
* 是否编码空格为+<br>
|
||||
* 如果为{@code true},则将空格编码为"+",此项只在"application/x-www-form-urlencoded"中使用<br>
|
||||
* 如果为{@code false},则空格编码为"%20",此项一般用于URL的Query部分(RFC3986规范)
|
||||
*/
|
||||
private boolean encodeSpaceAsPlus = false;
|
||||
|
||||
@ -130,7 +133,9 @@ public class PercentCodec implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否将空格编码为+
|
||||
* 是否将空格编码为+<br>
|
||||
* 如果为{@code true},则将空格编码为"+",此项只在"application/x-www-form-urlencoded"中使用<br>
|
||||
* 如果为{@code false},则空格编码为"%20",此项一般用于URL的Query部分(RFC3986规范)
|
||||
*
|
||||
* @param encodeSpaceAsPlus 是否将空格编码为+
|
||||
* @return this
|
||||
|
@ -100,6 +100,11 @@ public interface RegexPool {
|
||||
* 生日
|
||||
*/
|
||||
String BIRTHDAY = "^(\\d{2,4})([/\\-.年]?)(\\d{1,2})([/\\-.月]?)(\\d{1,2})日?$";
|
||||
/**
|
||||
* URI<br>
|
||||
* 定义见:https://www.ietf.org/rfc/rfc3986.html#appendix-B
|
||||
*/
|
||||
String URI = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?";
|
||||
/**
|
||||
* URL
|
||||
*/
|
||||
|
@ -0,0 +1,25 @@
|
||||
package cn.hutool.core.net;
|
||||
|
||||
import cn.hutool.core.codec.PercentCodec;
|
||||
|
||||
/**
|
||||
* application/x-www-form-urlencoded,遵循W3C HTML Form content types规范,如空格须转+,+须被编码<br>
|
||||
* 规范见:https://url.spec.whatwg.org/#urlencoded-serializing
|
||||
*
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public class FormUrlencoded {
|
||||
|
||||
/**
|
||||
* query中的value<br>
|
||||
* value不能包含"{@code &}",可以包含 "="
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_VALUE = PercentCodec.of(RFC3986.QUERY_PARAM_VALUE)
|
||||
.setEncodeSpaceAsPlus(true).removeSafe('+');
|
||||
|
||||
/**
|
||||
* query中的key<br>
|
||||
* key不能包含"{@code &}" 和 "="
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_NAME = QUERY_PARAM_VALUE.removeSafe('=');
|
||||
}
|
@ -3,7 +3,8 @@ package cn.hutool.core.net;
|
||||
import cn.hutool.core.codec.PercentCodec;
|
||||
|
||||
/**
|
||||
* rfc3986 : https://www.ietf.org/rfc/rfc3986.html 编码实现
|
||||
* rfc3986 : https://www.ietf.org/rfc/rfc3986.html 编码实现<br>
|
||||
* 定义见:https://www.ietf.org/rfc/rfc3986.html#appendix-A
|
||||
*
|
||||
* @author looly
|
||||
* @since 5.7.16
|
||||
@ -21,12 +22,14 @@ public class RFC3986 {
|
||||
public static final PercentCodec SUB_DELIMS = PercentCodec.of("!$&'()*+,;=");
|
||||
|
||||
/**
|
||||
* reserved = gen-delims / sub-delims
|
||||
* reserved = gen-delims / sub-delims<br>
|
||||
* see:https://www.ietf.org/rfc/rfc3986.html#section-2.2
|
||||
*/
|
||||
public static final PercentCodec RESERVED = GEN_DELIMS.orNew(SUB_DELIMS);
|
||||
|
||||
/**
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"<br>
|
||||
* see: https://www.ietf.org/rfc/rfc3986.html#section-2.3
|
||||
*/
|
||||
public static final PercentCodec UNRESERVED = PercentCodec.of(unreservedChars());
|
||||
|
||||
@ -36,7 +39,8 @@ public class RFC3986 {
|
||||
public static final PercentCodec PCHAR = UNRESERVED.orNew(SUB_DELIMS).or(PercentCodec.of(":@"));
|
||||
|
||||
/**
|
||||
* segment = pchar
|
||||
* segment = pchar<br>
|
||||
* see: https://www.ietf.org/rfc/rfc3986.html#section-3.3
|
||||
*/
|
||||
public static final PercentCodec SEGMENT = PCHAR;
|
||||
/**
|
||||
@ -60,15 +64,17 @@ public class RFC3986 {
|
||||
public static final PercentCodec FRAGMENT = QUERY;
|
||||
|
||||
/**
|
||||
* query中的key
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_NAME = PercentCodec.of(QUERY).removeSafe('&').removeSafe('=');
|
||||
|
||||
/**
|
||||
* query中的value
|
||||
* query中的value<br>
|
||||
* value不能包含"{@code &}",可以包含 "="
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_VALUE = PercentCodec.of(QUERY).removeSafe('&');
|
||||
|
||||
/**
|
||||
* query中的key<br>
|
||||
* key不能包含"{@code &}" 和 "="
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_NAME = QUERY_PARAM_VALUE.removeSafe('=');
|
||||
|
||||
/**
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
*
|
||||
|
@ -41,9 +41,10 @@ public class URLDecoder implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码
|
||||
* 解码<br>
|
||||
* 规则见:https://url.spec.whatwg.org/#urlencoded-parsing
|
||||
* <pre>
|
||||
* 1. 将+和%20转换为空格 ;
|
||||
* 1. 将+和%20转换为空格(" ");
|
||||
* 2. 将"%xy"转换为文本形式,xy是两位16进制的数值;
|
||||
* 3. 跳过不符合规范的%形式,直接输出
|
||||
* </pre>
|
||||
|
@ -7,7 +7,8 @@ import cn.hutool.core.util.StrUtil;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* URL编码工具
|
||||
* URL编码工具<br>
|
||||
* TODO 在6.x中移除此工具(无法很好区分URL编码和www-form编码)
|
||||
*
|
||||
* @since 5.7.13
|
||||
* @author looly
|
||||
|
@ -127,6 +127,9 @@ public class UrlPath {
|
||||
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (String segment : segments) {
|
||||
// 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义
|
||||
// path的第一部分允许有":",其余部分不允许
|
||||
// 在此处的Path部分特指host之后的部分,即不包含第一部分
|
||||
builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT_NZ_NC.encode(segment, charset));
|
||||
}
|
||||
if (withEngTag || StrUtil.isEmpty(builder)) {
|
||||
|
@ -144,48 +144,7 @@ public class UrlQuery {
|
||||
}
|
||||
}
|
||||
|
||||
final int len = queryStr.length();
|
||||
String name = null;
|
||||
int pos = 0; // 未处理字符开始位置
|
||||
int i; // 未处理字符结束位置
|
||||
char c; // 当前字符
|
||||
for (i = 0; i < len; i++) {
|
||||
c = queryStr.charAt(i);
|
||||
switch (c) {
|
||||
case '='://键和值的分界符
|
||||
if (null == name) {
|
||||
// name可以是""
|
||||
name = queryStr.substring(pos, i);
|
||||
// 开始位置从分节符后开始
|
||||
pos = i + 1;
|
||||
}
|
||||
// 当=不作为分界符时,按照普通字符对待
|
||||
break;
|
||||
case '&'://键值对之间的分界符
|
||||
addParam(name, queryStr.substring(pos, i), charset);
|
||||
name = null;
|
||||
if (i + 4 < len && "amp;".equals(queryStr.substring(i + 1, i + 5))) {
|
||||
// issue#850@Github,"&"转义为"&"
|
||||
i += 4;
|
||||
}
|
||||
// 开始位置从分节符后开始
|
||||
pos = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i - pos == len) {
|
||||
// 没有任何参数符号
|
||||
if (queryStr.startsWith("http") || queryStr.contains("/")) {
|
||||
// 可能为url路径,忽略之
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理结尾
|
||||
addParam(name, queryStr.substring(pos, i), charset);
|
||||
|
||||
return this;
|
||||
return doParse(queryStr, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -250,6 +209,60 @@ public class UrlQuery {
|
||||
return build(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析URL中的查询字符串<br>
|
||||
* 规则见:https://url.spec.whatwg.org/#urlencoded-parsing
|
||||
*
|
||||
* @param queryStr 查询字符串,类似于key1=v1&key2=&key3=v3
|
||||
* @param charset decode编码,null表示不做decode
|
||||
* @return this
|
||||
* @since 5.5.8
|
||||
*/
|
||||
private UrlQuery doParse(String queryStr, Charset charset) {
|
||||
final int len = queryStr.length();
|
||||
String name = null;
|
||||
int pos = 0; // 未处理字符开始位置
|
||||
int i; // 未处理字符结束位置
|
||||
char c; // 当前字符
|
||||
for (i = 0; i < len; i++) {
|
||||
c = queryStr.charAt(i);
|
||||
switch (c) {
|
||||
case '='://键和值的分界符
|
||||
if (null == name) {
|
||||
// name可以是""
|
||||
name = queryStr.substring(pos, i);
|
||||
// 开始位置从分节符后开始
|
||||
pos = i + 1;
|
||||
}
|
||||
// 当=不作为分界符时,按照普通字符对待
|
||||
break;
|
||||
case '&'://键值对之间的分界符
|
||||
addParam(name, queryStr.substring(pos, i), charset);
|
||||
name = null;
|
||||
if (i + 4 < len && "amp;".equals(queryStr.substring(i + 1, i + 5))) {
|
||||
// issue#850@Github,"&"转义为"&"
|
||||
i += 4;
|
||||
}
|
||||
// 开始位置从分节符后开始
|
||||
pos = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i - pos == len) {
|
||||
// 没有任何参数符号
|
||||
if (queryStr.startsWith("http") || queryStr.contains("/")) {
|
||||
// 可能为url路径,忽略之
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理结尾
|
||||
addParam(name, queryStr.substring(pos, i), charset);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象转换为字符串,用于URL的Query中
|
||||
*
|
||||
|
@ -319,7 +319,8 @@ public class URLUtil extends URLEncodeUtil {
|
||||
|
||||
/**
|
||||
* 解码application/x-www-form-urlencoded字符<br>
|
||||
* 将%开头的16进制表示的内容解码。
|
||||
* 将%开头的16进制表示的内容解码。<br>
|
||||
* 规则见:https://url.spec.whatwg.org/#urlencoded-parsing
|
||||
*
|
||||
* @param content 被解码内容
|
||||
* @param charset 编码,null表示不解码
|
||||
|
@ -3,6 +3,7 @@ package cn.hutool.core.net;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.net.url.UrlBuilder;
|
||||
import cn.hutool.core.net.url.UrlQuery;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@ -99,4 +100,18 @@ public class UrlQueryTest {
|
||||
query = URLUtil.buildQuery(map, StandardCharsets.UTF_8);
|
||||
Assert.assertEquals("password==&username%3D=SSM", query);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plusTest(){
|
||||
// 根据RFC3986,在URL中,+是安全字符,即此符号不转义
|
||||
final String a = UrlQuery.of(MapUtil.of("a+b", "1+2")).build(CharsetUtil.CHARSET_UTF_8);
|
||||
Assert.assertEquals("a+b=1+2", a);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spaceTest(){
|
||||
// 根据RFC3986,在URL中,空格编码为"%20"
|
||||
final String a = UrlQuery.of(MapUtil.of("a ", " ")).build(CharsetUtil.CHARSET_UTF_8);
|
||||
Assert.assertEquals("a%20=%20", a);
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.io.StreamProgress;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.net.RFC3986;
|
||||
import cn.hutool.core.net.url.UrlQuery;
|
||||
import cn.hutool.core.text.StrBuilder;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
@ -557,9 +558,10 @@ public class HttpUtil {
|
||||
if (null == name) {
|
||||
// 对于像&a&这类无参数值的字符串,我们将name为a的值设为""
|
||||
name = paramPart.substring(pos, i);
|
||||
builder.append(URLUtil.encodeQuery(name, charset)).append('=');
|
||||
builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=');
|
||||
} else {
|
||||
builder.append(URLUtil.encodeQuery(name, charset)).append('=').append(URLUtil.encodeQuery(paramPart.substring(pos, i), charset)).append('&');
|
||||
builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=')
|
||||
.append(RFC3986.QUERY_PARAM_VALUE.encode(paramPart.substring(pos, i), charset)).append('&');
|
||||
}
|
||||
name = null;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user