diff --git a/CHANGELOG.md b/CHANGELOG.md index ec5382341..5fd4d3e80 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ * 【core 】 修复CombinationAnnotationElement造成递归循环(issue#I5FQGW@Gitee) * 【core 】 修复Dict缺少putIfAbsent、computeIfAbsent问题(issue#I5FQGW@Gitee) * 【core 】 修复Console.log应该把异常信息输出位置错误问题(pr#716@Gitee) +* 【core 】 修复UrlBuilder无法配置末尾追加“/”问题(issue#2459@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-core/src/main/java/cn/hutool/core/net/url/UrlBuilder.java b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlBuilder.java index ccf54a4ee..76199023a 100755 --- a/hutool-core/src/main/java/cn/hutool/core/net/url/UrlBuilder.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlBuilder.java @@ -119,7 +119,7 @@ public final class UrlBuilder implements Builder { /** * 使用URL字符串构建UrlBuilder,默认使用UTF-8编码 * - * @param url URL字符串 + * @param url URL字符串 * @return UrlBuilder */ public static UrlBuilder of(String url) { @@ -318,6 +318,22 @@ public final class UrlBuilder implements Builder { 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相关设置 * @@ -501,7 +517,7 @@ public final class UrlBuilder implements Builder { final StringBuilder fileBuilder = new StringBuilder(); // path - fileBuilder.append(StrUtil.blankToDefault(getPathStr(), StrUtil.SLASH)); + fileBuilder.append(getPathStr()); // query final String query = getQueryStr(); diff --git a/hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java index 39a04ba71..868025993 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java @@ -142,12 +142,13 @@ public class UrlPath { */ public String build(Charset charset, boolean encodePercent) { if (CollUtil.isEmpty(this.segments)) { - return StrUtil.EMPTY; + // 没有节点的path取决于是否末尾追加/,如果不追加返回空串,否则返回/ + return withEngTag ? StrUtil.SLASH : StrUtil.EMPTY; } final char[] safeChars = encodePercent ? null : new char[]{'%'}; final StringBuilder builder = new StringBuilder(); - for (String segment : segments) { + for (final String segment : segments) { if(builder.length() == 0){ // 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义 // path的第一部分不允许有":",其余部分允许 @@ -157,12 +158,15 @@ public class UrlPath { builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT.encode(segment, charset, safeChars)); } } - if (StrUtil.isEmpty(builder)) { - // 空白追加是保证以/开头 - builder.append(CharUtil.SLASH); - }else if (withEngTag && false == StrUtil.endWith(builder, CharUtil.SLASH)) { - // 尾部没有/则追加,否则不追加 - builder.append(CharUtil.SLASH); + + if(withEngTag){ + if (StrUtil.isEmpty(builder)) { + // 空白追加是保证以/开头 + builder.append(CharUtil.SLASH); + }else if (false == StrUtil.endWith(builder, CharUtil.SLASH)) { + // 尾部没有/则追加,否则不追加 + builder.append(CharUtil.SLASH); + } } return builder.toString(); diff --git a/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java b/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java index 7d54c4503..5f869e70e 100644 --- a/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java @@ -16,20 +16,31 @@ public class UrlBuilderTest { @Test 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); } + @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 public void buildTest2() { // 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); } @Test public void testHost() { - String buildUrl = UrlBuilder.create() + final String buildUrl = UrlBuilder.create() .setScheme("https") .setHost("www.hutool.cn").build(); Assert.assertEquals("https://www.hutool.cn/", buildUrl); @@ -37,7 +48,7 @@ public class UrlBuilderTest { @Test public void testHostPort() { - String buildUrl = UrlBuilder.create() + final String buildUrl = UrlBuilder.create() .setScheme("https") .setHost("www.hutool.cn") .setPort(8080) @@ -87,7 +98,7 @@ public class UrlBuilderTest { @Test public void testFragment() { - String buildUrl = new UrlBuilder() + final String buildUrl = new UrlBuilder() .setScheme("https") .setHost("www.hutool.cn") .setFragment("abc").build(); @@ -96,7 +107,7 @@ public class UrlBuilderTest { @Test public void testChineseFragment() { - String buildUrl = new UrlBuilder() + final String buildUrl = new UrlBuilder() .setScheme("https") .setHost("www.hutool.cn") .setFragment("测试").build(); @@ -105,7 +116,7 @@ public class UrlBuilderTest { @Test public void testChineseFragmentWithPath() { - String buildUrl = new UrlBuilder() + final String buildUrl = new UrlBuilder() .setScheme("https") .setHost("www.hutool.cn") .addPath("/s") @@ -115,7 +126,7 @@ public class UrlBuilderTest { @Test public void testChineseFragmentWithPathAndQuery() { - String buildUrl = new UrlBuilder() + final String buildUrl = new UrlBuilder() .setScheme("https") .setHost("www.hutool.cn") .addPath("/s") @@ -194,7 +205,7 @@ public class UrlBuilderTest { @Test public void weixinUrlTest(){ - String urlStr = "https://mp.weixin.qq.com/s?" + + final String urlStr = "https://mp.weixin.qq.com/s?" + "__biz=MzI5NjkyNTIxMg==" + "&mid=100000465" + "&idx=1" + @@ -240,14 +251,14 @@ public class UrlBuilderTest { @Test 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); Assert.assertEquals(new URI(webUrl), urlBuilder.toURI()); } @Test 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); Assert.assertEquals("a=123&b=4?6&c=789", urlBuilder.getQueryStr()); } @@ -271,11 +282,11 @@ public class UrlBuilderTest { @Test 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); // 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()); } @@ -283,7 +294,7 @@ public class UrlBuilderTest { public void fragmentEncodeTest(){ // https://gitee.com/dromara/hutool/issues/I49KAL // 见: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); 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 // 在query中,"/"是不可转义字符 // 见: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); Assert.assertEquals(url, urlBuilder.toString()); } @Test public void addPathEncodeTest(){ - String url = UrlBuilder.create() + final String url = UrlBuilder.create() .setScheme("https") .setHost("domain.cn") .addPath("api") @@ -317,7 +328,7 @@ public class UrlBuilderTest { @Test public void addPathEncodeTest2(){ // https://github.com/dromara/hutool/issues/1912 - String url = UrlBuilder.create() + final String url = UrlBuilder.create() .setScheme("https") .setHost("domain.cn") .addPath("/api/xxx/bbb") @@ -328,14 +339,14 @@ public class UrlBuilderTest { @Test 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); Assert.assertEquals(url, of.toString()); } @Test 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); Assert.assertEquals(url, builder.toString()); } @@ -343,7 +354,7 @@ public class UrlBuilderTest { @Test public void fragmentTest(){ // 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); Assert.assertEquals(url, builder.toString()); @@ -352,7 +363,7 @@ public class UrlBuilderTest { @Test public void fragmentAppendParamTest(){ // 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); builder.setFragment(builder.getFragment() + "?timestamp=1640391380204"); Assert.assertEquals("https://www.hutool.cn/#/a/b?timestamp=1640391380204", builder.toString()); @@ -360,7 +371,7 @@ public class UrlBuilderTest { @Test public void paramWithPlusTest(){ - String url = "http://127.0.0.1/?" + + final String url = "http://127.0.0.1/?" + "Expires=1642734164&" + "security-token=CAIS+AF1q6Ft5B2yfSjIr5fYEeju1b1ggpPee2KGpjlgQtdfl43urjz2IHtKdXRvBu8Xs" + "/4wnmxX7f4YlqB6T55OSAmcNZEoPwKpT4zmMeT7oMWQweEurv" + @@ -376,7 +387,7 @@ public class UrlBuilderTest { @Test public void issueI4Z2ETTest(){ // =是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" + "&OSSAccessKeyId=STS.NTZ9hvqPSLG8ENknz2YaByLKj" + "&Signature=oYUu26JufAyPY4PdzaOp1x4sr4Q%3D"; @@ -387,22 +398,22 @@ public class UrlBuilderTest { @Test 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(); Assert.assertEquals(url, build); } @Test 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(); Assert.assertEquals(url, build); } @Test public void issueI50NHQTest(){ - String url = "http://127.0.0.1/devicerecord/list"; - HashMap params = new LinkedHashMap<>(); + final String url = "http://127.0.0.1/devicerecord/list"; + final HashMap params = new LinkedHashMap<>(); params.put("start", "2022-03-31 00:00:00"); params.put("end", "2022-03-31 23:59:59"); params.put("page", 1); @@ -422,7 +433,7 @@ public class UrlBuilderTest { public void issue2243Test(){ // 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(); Assert.assertEquals(url, s); } @@ -430,7 +441,7 @@ public class UrlBuilderTest { @Test public void issueI51T0VTest(){ // &自动转换为& - String url = "https://hutool.cn/a.mp3?Expires=1652423884&key=JMv2rKNc7Pz&sign=12zva00BpVqgZcX1wcb%2BrmN7H3E%3D"; + final String url = "https://hutool.cn/a.mp3?Expires=1652423884&key=JMv2rKNc7Pz&sign=12zva00BpVqgZcX1wcb%2BrmN7H3E%3D"; final UrlBuilder of = UrlBuilder.of(url, null); Assert.assertEquals(url.replace("&", "&"), of.toString()); }