修复UrlBuilder无法配置末尾追加“/”问题

This commit is contained in:
Looly 2022-07-20 13:13:14 +08:00
parent 1fcee08263
commit 88c36b8bfa
4 changed files with 71 additions and 39 deletions

View File

@ -35,6 +35,7 @@
* 【core 】 修复CombinationAnnotationElement造成递归循环issue#I5FQGW@Gitee * 【core 】 修复CombinationAnnotationElement造成递归循环issue#I5FQGW@Gitee
* 【core 】 修复Dict缺少putIfAbsent、computeIfAbsent问题issue#I5FQGW@Gitee * 【core 】 修复Dict缺少putIfAbsent、computeIfAbsent问题issue#I5FQGW@Gitee
* 【core 】 修复Console.log应该把异常信息输出位置错误问题pr#716@Gitee * 【core 】 修复Console.log应该把异常信息输出位置错误问题pr#716@Gitee
* 【core 】 修复UrlBuilder无法配置末尾追加“/”问题issue#2459@Github
------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------

View File

@ -318,6 +318,22 @@ public final class UrlBuilder implements Builder<String> {
return null == this.path ? StrUtil.SLASH : this.path.build(charset, this.needEncodePercent); return null == this.path ? StrUtil.SLASH : this.path.build(charset, this.needEncodePercent);
} }
/**
* 是否path的末尾加 /
*
* @param withEngTag 是否path的末尾加 /
* @return this
* @since 5.8.5
*/
public UrlBuilder setWithEndTag(boolean withEngTag) {
if (null == this.path) {
this.path = new UrlPath();
}
this.path.setWithEndTag(withEngTag);
return this;
}
/** /**
* 设置路径例如/aa/bb/cc将覆盖之前所有的path相关设置 * 设置路径例如/aa/bb/cc将覆盖之前所有的path相关设置
* *
@ -501,7 +517,7 @@ public final class UrlBuilder implements Builder<String> {
final StringBuilder fileBuilder = new StringBuilder(); final StringBuilder fileBuilder = new StringBuilder();
// path // path
fileBuilder.append(StrUtil.blankToDefault(getPathStr(), StrUtil.SLASH)); fileBuilder.append(getPathStr());
// query // query
final String query = getQueryStr(); final String query = getQueryStr();

View File

@ -142,12 +142,13 @@ public class UrlPath {
*/ */
public String build(Charset charset, boolean encodePercent) { public String build(Charset charset, boolean encodePercent) {
if (CollUtil.isEmpty(this.segments)) { if (CollUtil.isEmpty(this.segments)) {
return StrUtil.EMPTY; // 没有节点的path取决于是否末尾追加/如果不追加返回空串否则返回/
return withEngTag ? StrUtil.SLASH : StrUtil.EMPTY;
} }
final char[] safeChars = encodePercent ? null : new char[]{'%'}; final char[] safeChars = encodePercent ? null : new char[]{'%'};
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
for (String segment : segments) { for (final String segment : segments) {
if(builder.length() == 0){ if(builder.length() == 0){
// 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义 // 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义
// path的第一部分不允许有":"其余部分允许 // path的第一部分不允许有":"其余部分允许
@ -157,13 +158,16 @@ public class UrlPath {
builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT.encode(segment, charset, safeChars)); builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT.encode(segment, charset, safeChars));
} }
} }
if(withEngTag){
if (StrUtil.isEmpty(builder)) { if (StrUtil.isEmpty(builder)) {
// 空白追加是保证以/开头 // 空白追加是保证以/开头
builder.append(CharUtil.SLASH); builder.append(CharUtil.SLASH);
}else if (withEngTag && false == StrUtil.endWith(builder, CharUtil.SLASH)) { }else if (false == StrUtil.endWith(builder, CharUtil.SLASH)) {
// 尾部没有/则追加否则不追加 // 尾部没有/则追加否则不追加
builder.append(CharUtil.SLASH); builder.append(CharUtil.SLASH);
} }
}
return builder.toString(); return builder.toString();
} }

View File

@ -16,20 +16,31 @@ public class UrlBuilderTest {
@Test @Test
public void buildTest() { public void buildTest() {
String buildUrl = UrlBuilder.create().setHost("www.hutool.cn").build(); final String buildUrl = UrlBuilder.create().setHost("www.hutool.cn").build();
Assert.assertEquals("http://www.hutool.cn/", buildUrl); Assert.assertEquals("http://www.hutool.cn/", buildUrl);
} }
@Test
public void buildWithoutSlashTest(){
// https://github.com/dromara/hutool/issues/2459
String buildUrl = UrlBuilder.create().setScheme("http").setHost("192.168.1.1").setPort(8080).setWithEndTag(false).build();
Assert.assertEquals("http://192.168.1.1:8080", buildUrl);
buildUrl = UrlBuilder.create().setScheme("http").setHost("192.168.1.1").setPort(8080).addQuery("url", "http://192.168.1.1/test/1")
.setWithEndTag(false).build();
Assert.assertEquals("http://192.168.1.1:8080?url=http://192.168.1.1/test/1", buildUrl);
}
@Test @Test
public void buildTest2() { public void buildTest2() {
// path中的+不做处理 // path中的+不做处理
String buildUrl = UrlBuilder.ofHttp("http://www.hutool.cn/+8618888888888", CharsetUtil.CHARSET_UTF_8).build(); final String buildUrl = UrlBuilder.ofHttp("http://www.hutool.cn/+8618888888888", CharsetUtil.CHARSET_UTF_8).build();
Assert.assertEquals("http://www.hutool.cn/+8618888888888", buildUrl); Assert.assertEquals("http://www.hutool.cn/+8618888888888", buildUrl);
} }
@Test @Test
public void testHost() { public void testHost() {
String buildUrl = UrlBuilder.create() final String buildUrl = UrlBuilder.create()
.setScheme("https") .setScheme("https")
.setHost("www.hutool.cn").build(); .setHost("www.hutool.cn").build();
Assert.assertEquals("https://www.hutool.cn/", buildUrl); Assert.assertEquals("https://www.hutool.cn/", buildUrl);
@ -37,7 +48,7 @@ public class UrlBuilderTest {
@Test @Test
public void testHostPort() { public void testHostPort() {
String buildUrl = UrlBuilder.create() final String buildUrl = UrlBuilder.create()
.setScheme("https") .setScheme("https")
.setHost("www.hutool.cn") .setHost("www.hutool.cn")
.setPort(8080) .setPort(8080)
@ -87,7 +98,7 @@ public class UrlBuilderTest {
@Test @Test
public void testFragment() { public void testFragment() {
String buildUrl = new UrlBuilder() final String buildUrl = new UrlBuilder()
.setScheme("https") .setScheme("https")
.setHost("www.hutool.cn") .setHost("www.hutool.cn")
.setFragment("abc").build(); .setFragment("abc").build();
@ -96,7 +107,7 @@ public class UrlBuilderTest {
@Test @Test
public void testChineseFragment() { public void testChineseFragment() {
String buildUrl = new UrlBuilder() final String buildUrl = new UrlBuilder()
.setScheme("https") .setScheme("https")
.setHost("www.hutool.cn") .setHost("www.hutool.cn")
.setFragment("测试").build(); .setFragment("测试").build();
@ -105,7 +116,7 @@ public class UrlBuilderTest {
@Test @Test
public void testChineseFragmentWithPath() { public void testChineseFragmentWithPath() {
String buildUrl = new UrlBuilder() final String buildUrl = new UrlBuilder()
.setScheme("https") .setScheme("https")
.setHost("www.hutool.cn") .setHost("www.hutool.cn")
.addPath("/s") .addPath("/s")
@ -115,7 +126,7 @@ public class UrlBuilderTest {
@Test @Test
public void testChineseFragmentWithPathAndQuery() { public void testChineseFragmentWithPathAndQuery() {
String buildUrl = new UrlBuilder() final String buildUrl = new UrlBuilder()
.setScheme("https") .setScheme("https")
.setHost("www.hutool.cn") .setHost("www.hutool.cn")
.addPath("/s") .addPath("/s")
@ -194,7 +205,7 @@ public class UrlBuilderTest {
@Test @Test
public void weixinUrlTest(){ public void weixinUrlTest(){
String urlStr = "https://mp.weixin.qq.com/s?" + final String urlStr = "https://mp.weixin.qq.com/s?" +
"__biz=MzI5NjkyNTIxMg==" + "__biz=MzI5NjkyNTIxMg==" +
"&amp;mid=100000465" + "&amp;mid=100000465" +
"&amp;idx=1" + "&amp;idx=1" +
@ -240,14 +251,14 @@ public class UrlBuilderTest {
@Test @Test
public void toURITest() throws URISyntaxException { public void toURITest() throws URISyntaxException {
String webUrl = "http://exmple.com/patha/pathb?a=123"; // 报错数据 final String webUrl = "http://exmple.com/patha/pathb?a=123"; // 报错数据
final UrlBuilder urlBuilder = UrlBuilder.of(webUrl, StandardCharsets.UTF_8); final UrlBuilder urlBuilder = UrlBuilder.of(webUrl, StandardCharsets.UTF_8);
Assert.assertEquals(new URI(webUrl), urlBuilder.toURI()); Assert.assertEquals(new URI(webUrl), urlBuilder.toURI());
} }
@Test @Test
public void testEncodeInQuery() { public void testEncodeInQuery() {
String webUrl = "http://exmple.com/patha/pathb?a=123&b=4?6&c=789"; // b=4?6 参数中有未编码的 final String webUrl = "http://exmple.com/patha/pathb?a=123&b=4?6&c=789"; // b=4?6 参数中有未编码的
final UrlBuilder urlBuilder = UrlBuilder.of(webUrl, StandardCharsets.UTF_8); final UrlBuilder urlBuilder = UrlBuilder.of(webUrl, StandardCharsets.UTF_8);
Assert.assertEquals("a=123&b=4?6&c=789", urlBuilder.getQueryStr()); Assert.assertEquals("a=123&b=4?6&c=789", urlBuilder.getQueryStr());
} }
@ -271,11 +282,11 @@ public class UrlBuilderTest {
@Test @Test
public void gimg2Test(){ public void gimg2Test(){
String url = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http%3A%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea"; final String url = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http%3A%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea";
final UrlBuilder urlBuilder = UrlBuilder.of(url); final UrlBuilder urlBuilder = UrlBuilder.of(url);
// PATH除了第一个path外:是允许的 // PATH除了第一个path外:是允许的
String url2 = "https://gimg2.baidu.com/image_search/src=http:%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http:%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea"; final String url2 = "https://gimg2.baidu.com/image_search/src=http:%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http:%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea";
Assert.assertEquals(url2, urlBuilder.toString()); Assert.assertEquals(url2, urlBuilder.toString());
} }
@ -283,7 +294,7 @@ public class UrlBuilderTest {
public void fragmentEncodeTest(){ public void fragmentEncodeTest(){
// https://gitee.com/dromara/hutool/issues/I49KAL // https://gitee.com/dromara/hutool/issues/I49KAL
// https://stackoverflow.com/questions/26088849/url-fragment-allowed-characters // https://stackoverflow.com/questions/26088849/url-fragment-allowed-characters
String url = "https://hutool.cn/docs/#/?id=简介"; final String url = "https://hutool.cn/docs/#/?id=简介";
UrlBuilder urlBuilder = UrlBuilder.ofHttp(url); UrlBuilder urlBuilder = UrlBuilder.ofHttp(url);
Assert.assertEquals("https://hutool.cn/docs/#/?id=%E7%AE%80%E4%BB%8B", urlBuilder.toString()); Assert.assertEquals("https://hutool.cn/docs/#/?id=%E7%AE%80%E4%BB%8B", urlBuilder.toString());
@ -296,14 +307,14 @@ public class UrlBuilderTest {
// https://github.com/dromara/hutool/issues/1904 // https://github.com/dromara/hutool/issues/1904
// 在query中"/"是不可转义字符 // 在query中"/"是不可转义字符
// https://www.rfc-editor.org/rfc/rfc3986.html#section-3.4 // https://www.rfc-editor.org/rfc/rfc3986.html#section-3.4
String url = "https://invoice.maycur.com/2b27a802-8423-4d41-86f5-63a6b259f61e.xlsx?download/2b27a802-8423-4d41-86f5-63a6b259f61e.xlsx&e=1630491088"; final String url = "https://invoice.maycur.com/2b27a802-8423-4d41-86f5-63a6b259f61e.xlsx?download/2b27a802-8423-4d41-86f5-63a6b259f61e.xlsx&e=1630491088";
final UrlBuilder urlBuilder = UrlBuilder.ofHttp(url); final UrlBuilder urlBuilder = UrlBuilder.ofHttp(url);
Assert.assertEquals(url, urlBuilder.toString()); Assert.assertEquals(url, urlBuilder.toString());
} }
@Test @Test
public void addPathEncodeTest(){ public void addPathEncodeTest(){
String url = UrlBuilder.create() final String url = UrlBuilder.create()
.setScheme("https") .setScheme("https")
.setHost("domain.cn") .setHost("domain.cn")
.addPath("api") .addPath("api")
@ -317,7 +328,7 @@ public class UrlBuilderTest {
@Test @Test
public void addPathEncodeTest2(){ public void addPathEncodeTest2(){
// https://github.com/dromara/hutool/issues/1912 // https://github.com/dromara/hutool/issues/1912
String url = UrlBuilder.create() final String url = UrlBuilder.create()
.setScheme("https") .setScheme("https")
.setHost("domain.cn") .setHost("domain.cn")
.addPath("/api/xxx/bbb") .addPath("/api/xxx/bbb")
@ -328,14 +339,14 @@ public class UrlBuilderTest {
@Test @Test
public void percent2BTest(){ public void percent2BTest(){
String url = "http://xxx.cn/a?Signature=3R013Bj9Uq4YeISzAs2iC%2BTVCL8%3D"; final String url = "http://xxx.cn/a?Signature=3R013Bj9Uq4YeISzAs2iC%2BTVCL8%3D";
final UrlBuilder of = UrlBuilder.ofHttpWithoutEncode(url); final UrlBuilder of = UrlBuilder.ofHttpWithoutEncode(url);
Assert.assertEquals(url, of.toString()); Assert.assertEquals(url, of.toString());
} }
@Test @Test
public void paramTest(){ public void paramTest(){
String url = "http://ci.xiaohongshu.com/spectrum/c136c98aa2047babe25b994a26ffa7b492bd8058?imageMogr2/thumbnail/x800/format/jpg"; final String url = "http://ci.xiaohongshu.com/spectrum/c136c98aa2047babe25b994a26ffa7b492bd8058?imageMogr2/thumbnail/x800/format/jpg";
final UrlBuilder builder = UrlBuilder.ofHttp(url); final UrlBuilder builder = UrlBuilder.ofHttp(url);
Assert.assertEquals(url, builder.toString()); Assert.assertEquals(url, builder.toString());
} }
@ -343,7 +354,7 @@ public class UrlBuilderTest {
@Test @Test
public void fragmentTest(){ public void fragmentTest(){
// https://gitee.com/dromara/hutool/issues/I49KAL#note_8060874 // https://gitee.com/dromara/hutool/issues/I49KAL#note_8060874
String url = "https://www.hutool.cn/#/a/b?timestamp=1640391380204"; final String url = "https://www.hutool.cn/#/a/b?timestamp=1640391380204";
final UrlBuilder builder = UrlBuilder.ofHttp(url); final UrlBuilder builder = UrlBuilder.ofHttp(url);
Assert.assertEquals(url, builder.toString()); Assert.assertEquals(url, builder.toString());
@ -352,7 +363,7 @@ public class UrlBuilderTest {
@Test @Test
public void fragmentAppendParamTest(){ public void fragmentAppendParamTest(){
// https://gitee.com/dromara/hutool/issues/I49KAL#note_8060874 // https://gitee.com/dromara/hutool/issues/I49KAL#note_8060874
String url = "https://www.hutool.cn/#/a/b"; final String url = "https://www.hutool.cn/#/a/b";
final UrlBuilder builder = UrlBuilder.ofHttp(url); final UrlBuilder builder = UrlBuilder.ofHttp(url);
builder.setFragment(builder.getFragment() + "?timestamp=1640391380204"); builder.setFragment(builder.getFragment() + "?timestamp=1640391380204");
Assert.assertEquals("https://www.hutool.cn/#/a/b?timestamp=1640391380204", builder.toString()); Assert.assertEquals("https://www.hutool.cn/#/a/b?timestamp=1640391380204", builder.toString());
@ -360,7 +371,7 @@ public class UrlBuilderTest {
@Test @Test
public void paramWithPlusTest(){ public void paramWithPlusTest(){
String url = "http://127.0.0.1/?" + final String url = "http://127.0.0.1/?" +
"Expires=1642734164&" + "Expires=1642734164&" +
"security-token=CAIS+AF1q6Ft5B2yfSjIr5fYEeju1b1ggpPee2KGpjlgQtdfl43urjz2IHtKdXRvBu8Xs" + "security-token=CAIS+AF1q6Ft5B2yfSjIr5fYEeju1b1ggpPee2KGpjlgQtdfl43urjz2IHtKdXRvBu8Xs" +
"/4wnmxX7f4YlqB6T55OSAmcNZEoPwKpT4zmMeT7oMWQweEurv" + "/4wnmxX7f4YlqB6T55OSAmcNZEoPwKpT4zmMeT7oMWQweEurv" +
@ -376,7 +387,7 @@ public class UrlBuilderTest {
@Test @Test
public void issueI4Z2ETTest(){ public void issueI4Z2ETTest(){
// =是url参数值中的合法字符但是某些URL强制编码了 // =是url参数值中的合法字符但是某些URL强制编码了
String url = "http://dsl-fd.dslbuy.com/fssc/1647947565522.pdf?" + final String url = "http://dsl-fd.dslbuy.com/fssc/1647947565522.pdf?" +
"Expires=1647949365" + "Expires=1647949365" +
"&OSSAccessKeyId=STS.NTZ9hvqPSLG8ENknz2YaByLKj" + "&OSSAccessKeyId=STS.NTZ9hvqPSLG8ENknz2YaByLKj" +
"&Signature=oYUu26JufAyPY4PdzaOp1x4sr4Q%3D"; "&Signature=oYUu26JufAyPY4PdzaOp1x4sr4Q%3D";
@ -387,22 +398,22 @@ public class UrlBuilderTest {
@Test @Test
public void issue2215Test(){ public void issue2215Test(){
String url = "https://hutool.cn/v1/104303371/messages:send"; final String url = "https://hutool.cn/v1/104303371/messages:send";
final String build = UrlBuilder.of(url).build(); final String build = UrlBuilder.of(url).build();
Assert.assertEquals(url, build); Assert.assertEquals(url, build);
} }
@Test @Test
public void issuesI4Z2ETTest(){ public void issuesI4Z2ETTest(){
String url = "http://hutool.cn/2022/03/09/123.zip?Expires=1648704684&OSSAccessKeyId=LTAI4FncgaVtwZGBnYHHi8ox&Signature=%2BK%2B%3D"; final String url = "http://hutool.cn/2022/03/09/123.zip?Expires=1648704684&OSSAccessKeyId=LTAI4FncgaVtwZGBnYHHi8ox&Signature=%2BK%2B%3D";
final String build = UrlBuilder.of(url, null).build(); final String build = UrlBuilder.of(url, null).build();
Assert.assertEquals(url, build); Assert.assertEquals(url, build);
} }
@Test @Test
public void issueI50NHQTest(){ public void issueI50NHQTest(){
String url = "http://127.0.0.1/devicerecord/list"; final String url = "http://127.0.0.1/devicerecord/list";
HashMap<String, Object> params = new LinkedHashMap<>(); final HashMap<String, Object> params = new LinkedHashMap<>();
params.put("start", "2022-03-31 00:00:00"); params.put("start", "2022-03-31 00:00:00");
params.put("end", "2022-03-31 23:59:59"); params.put("end", "2022-03-31 23:59:59");
params.put("page", 1); params.put("page", 1);
@ -422,7 +433,7 @@ public class UrlBuilderTest {
public void issue2243Test(){ public void issue2243Test(){
// https://github.com/dromara/hutool/issues/2243 // https://github.com/dromara/hutool/issues/2243
// 如果用户已经做了%编码不应该重复编码 // 如果用户已经做了%编码不应该重复编码
String url = "https://hutool.cn/v1.0?privateNum=%2B8616512884988"; final String url = "https://hutool.cn/v1.0?privateNum=%2B8616512884988";
final String s = UrlBuilder.of(url, null).setCharset(CharsetUtil.CHARSET_UTF_8).toString(); final String s = UrlBuilder.of(url, null).setCharset(CharsetUtil.CHARSET_UTF_8).toString();
Assert.assertEquals(url, s); Assert.assertEquals(url, s);
} }
@ -430,7 +441,7 @@ public class UrlBuilderTest {
@Test @Test
public void issueI51T0VTest(){ public void issueI51T0VTest(){
// &amp;自动转换为& // &amp;自动转换为&
String url = "https://hutool.cn/a.mp3?Expires=1652423884&amp;key=JMv2rKNc7Pz&amp;sign=12zva00BpVqgZcX1wcb%2BrmN7H3E%3D"; final String url = "https://hutool.cn/a.mp3?Expires=1652423884&amp;key=JMv2rKNc7Pz&amp;sign=12zva00BpVqgZcX1wcb%2BrmN7H3E%3D";
final UrlBuilder of = UrlBuilder.of(url, null); final UrlBuilder of = UrlBuilder.of(url, null);
Assert.assertEquals(url.replace("&amp;", "&"), of.toString()); Assert.assertEquals(url.replace("&amp;", "&"), of.toString());
} }