From ca7c407a1c975cf5d4ce9e9be883d252163ed454 Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 16 Apr 2020 01:13:58 +0800 Subject: [PATCH] add UrlDecoder --- CHANGELOG.md | 4 + .../java/cn/hutool/core/map/TableMap.java | 33 +- .../java/cn/hutool/core/net/URLDecoder.java | 73 +++ .../java/cn/hutool/core/net/URLEncoder.java | 14 +- .../cn/hutool/core/net/url/UrlBuilder.java | 495 ++++++++++++++++++ .../java/cn/hutool/core/net/url/UrlPath.java | 172 ++++++ .../java/cn/hutool/core/net/url/UrlQuery.java | 245 +++++++++ .../cn/hutool/core/net/url/package-info.java | 7 + .../java/cn/hutool/core/util/CharUtil.java | 11 + .../java/cn/hutool/core/util/StrUtil.java | 2 +- .../java/cn/hutool/core/util/URLUtil.java | 181 ++++--- .../cn/hutool/core/bean/BeanUtilTest.java | 2 - .../cn/hutool/core/net/UrlBuilderTest.java | 173 ++++++ .../java/cn/hutool/crypto/symmetric/AES.java | 10 +- .../main/java/cn/hutool/http/HttpRequest.java | 43 +- .../java/cn/hutool/http/HttpResponse.java | 8 +- .../main/java/cn/hutool/http/HttpUtil.java | 144 +---- .../cn/hutool/http/test/HttpUtilTest.java | 14 +- 18 files changed, 1402 insertions(+), 229 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/URLDecoder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/url/UrlBuilder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/url/UrlQuery.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/url/package-info.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index dcdfb9f07..dc5cabd29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,15 @@ ### 新特性 * 【core 】 ListUtil、MapUtil、CollUtil增加empty方法 * 【poi 】 调整别名策略,clearHeaderAlias和addHeaderAlias同时清除aliasComparator(issue#828@Github) +* 【core 】 修改StrUtil.equals逻辑,改为contentEquals +* 【core 】 增加URLUtil.UrlDecoder ### Bug修复 * 【json 】 修复解析JSON字符串时配置无法传递问题 * 【core 】 修复ServletUtil.readCookieMap空指针问题(issue#827@Github) * 【crypto 】 修复SM2中检查密钥导致的问题(issue#I1EC47@Gitee) +* 【core 】 修复TableMap.isEmpty判断问题 +* 【http 】 修复编码后的URL传入导致二次编码的问题(issue#I1EIMN@Gitee) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java b/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java index 8fcfd9919..3671b5026 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java @@ -2,7 +2,6 @@ package cn.hutool.core.map; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import java.io.Serializable; @@ -10,6 +9,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -24,7 +25,7 @@ import java.util.Set; * @param 值类型 * @author looly */ -public class TableMap implements Map, Serializable { +public class TableMap implements Map, Iterable>, Serializable { private static final long serialVersionUID = 1L; private final List keys; @@ -58,7 +59,7 @@ public class TableMap implements Map, Serializable { @Override public boolean isEmpty() { - return ArrayUtil.isEmpty(keys); + return CollUtil.isEmpty(keys); } @Override @@ -159,13 +160,37 @@ public class TableMap implements Map, Serializable { @SuppressWarnings("NullableProblems") @Override public Set> entrySet() { - HashSet> hashSet = new HashSet<>(); + final Set> hashSet = new LinkedHashSet<>(); for (int i = 0; i < size(); i++) { hashSet.add(new Entry<>(keys.get(i), values.get(i))); } return hashSet; } + @Override + public Iterator> iterator() { + return new Iterator>() { + private final Iterator keysIter = keys.iterator(); + private final Iterator valuesIter = values.iterator(); + + @Override + public boolean hasNext() { + return keysIter.hasNext() && valuesIter.hasNext(); + } + + @Override + public Map.Entry next() { + return new Entry<>(keysIter.next(), valuesIter.next()); + } + + @Override + public void remove() { + keysIter.remove(); + valuesIter.remove(); + } + }; + } + private static class Entry implements Map.Entry { private final K key; diff --git a/hutool-core/src/main/java/cn/hutool/core/net/URLDecoder.java b/hutool-core/src/main/java/cn/hutool/core/net/URLDecoder.java new file mode 100644 index 000000000..9509fa72c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/URLDecoder.java @@ -0,0 +1,73 @@ +package cn.hutool.core.net; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.ByteArrayOutputStream; +import java.io.Serializable; +import java.nio.charset.Charset; + +/** + * URL解码,数据内容的类型是 application/x-www-form-urlencoded。 + * + *
+ * 1. 将%20转换为空格 ;
+ * 2. 将"%xy"转换为文本形式,xy是两位16进制的数值;
+ * 3. 跳过不符合规范的%形式,直接输出
+ * 
+ * + * @author looly + */ +public class URLDecoder implements Serializable { + private static final long serialVersionUID = 1L; + + private static final byte ESCAPE_CHAR = '%'; + + /** + * 解码 + * + * @param str 包含URL编码后的字符串 + * @param charset 编码 + * @return 解码后的字符串 + */ + public static String decode(String str, Charset charset) { + return StrUtil.str(decode(StrUtil.bytes(str, charset)), charset); + } + + /** + * 解码 + * + * @param bytes url编码的bytes + * @return 解码后的bytes + */ + public static byte[] decode(byte[] bytes) { + if (bytes == null) { + return null; + } + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(bytes.length); + int b; + for (int i = 0; i < bytes.length; i++) { + b = bytes[i]; + if (b == '+') { + buffer.write(CharUtil.SPACE); + } else if (b == ESCAPE_CHAR) { + if (i + 1 < bytes.length) { + final int u = CharUtil.digit16(bytes[i + 1]); + if (u >= 0 && i + 2 < bytes.length) { + final int l = CharUtil.digit16(bytes[i + 2]); + if (l >= 0) { + buffer.write((char) ((u << 4) + l)); + i += 2; + continue; + } + } + } + // 跳过不符合规范的%形式 + buffer.write(b); + } else { + buffer.write(b); + } + } + return buffer.toByteArray(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java b/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java index 096b0025d..c01365f00 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java @@ -1,5 +1,8 @@ package cn.hutool.core.net; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.HexUtil; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; @@ -7,9 +10,6 @@ import java.io.Serializable; import java.nio.charset.Charset; import java.util.BitSet; -import cn.hutool.core.util.CharUtil; -import cn.hutool.core.util.HexUtil; - /** * URL编码,数据内容的类型是 application/x-www-form-urlencoded。 * @@ -17,7 +17,6 @@ import cn.hutool.core.util.HexUtil; * 1.字符"a"-"z","A"-"Z","0"-"9",".","-","*",和"_" 都不会被编码; * 2.将空格转换为%20 ; * 3.将非文本内容转换成"%xy"的形式,xy是两位16进制的数值; - * 4.在每个 name=value 对之间放置 & 符号。 * * * @author looly, @@ -196,10 +195,8 @@ public class URLEncoder implements Serializable{ * @return 编码后的字符串 */ public String encode(String path, Charset charset) { - - int maxBytesPerChar = 10; final StringBuilder rewrittenPath = new StringBuilder(path.length()); - ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); + ByteArrayOutputStream buf = new ByteArrayOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(buf, charset); int c; @@ -221,9 +218,8 @@ public class URLEncoder implements Serializable{ } byte[] ba = buf.toByteArray(); - for (int j = 0; j < ba.length; j++) { + for (byte toEncode : ba) { // Converting each byte in the buffer - byte toEncode = ba[j]; rewrittenPath.append('%'); HexUtil.appendHex(rewrittenPath, toEncode, false); } 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 new file mode 100644 index 000000000..5fb76ff06 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlBuilder.java @@ -0,0 +1,495 @@ +package cn.hutool.core.net.url; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLStreamHandler; +import java.nio.charset.Charset; + +/** + * URL 生成器,格式形如: + *
+ * [scheme:]scheme-specific-part[#fragment]
+ * [scheme:][//authority][path][?query][#fragment]
+ * [scheme:][//host:port][path][?query][#fragment]
+ * 
+ * + * @author looly + * @see Uniform Resource Identifier + * @since 5.3.1 + */ +public final class UrlBuilder implements Serializable { + private static final long serialVersionUID = 1L; + private static final String DEFAULT_SCHEME = "http"; + + /** + * 协议,例如http + */ + private String scheme; + /** + * 主机,例如127.0.0.1 + */ + private String host; + /** + * 端口,默认-1 + */ + private int port = -1; + /** + * 路径,例如/aa/bb/cc + */ + private UrlPath path; + /** + * 查询语句,例如a=1&b=2 + */ + private UrlQuery query; + /** + * 标识符,例如#后边的部分 + */ + private String fragment; + + /** + * 编码,用于URLEncode和URLDecode + */ + private Charset charset; + + /** + * 使用URI构建UrlBuilder + * + * @param uri URI + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(URI uri, Charset charset) { + return of(uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath(), uri.getRawQuery(), uri.getFragment(), charset); + } + + /** + * 使用URL字符串构建UrlBuilder + * + * @param httpUrl URL字符串 + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder ofHttp(String httpUrl, Charset charset) { + Assert.notBlank(httpUrl, "Http url must be not blank!"); + + final int sepIndex = httpUrl.indexOf("://"); + if (sepIndex < 0) { + httpUrl = "http://" + httpUrl.trim(); + } + return of(httpUrl, charset); + } + + /** + * 使用URL字符串构建UrlBuilder + * + * @param url URL字符串 + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(String url, Charset charset) { + Assert.notBlank(url, "Url must be not blank!"); + return of(URLUtil.url(url.trim()), charset); + } + + /** + * 使用URL构建UrlBuilder + * + * @param url URL + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(URL url, Charset charset) { + return of(url.getProtocol(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef(), charset); + } + + /** + * 构建UrlBuilder + * + * @param scheme 协议,默认http + * @param host 主机,例如127.0.0.1 + * @param port 端口,-1表示默认端口 + * @param path 路径,例如/aa/bb/cc + * @param query 查询,例如a=1&b=2 + * @param fragment 标识符例如#后边的部分 + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(String scheme, String host, int port, String path, String query, String fragment, Charset charset) { + return of(scheme, host, port, UrlPath.of(path, charset), UrlQuery.of(query, charset), fragment, charset); + } + + /** + * 构建UrlBuilder + * + * @param scheme 协议,默认http + * @param host 主机,例如127.0.0.1 + * @param port 端口,-1表示默认端口 + * @param path 路径,例如/aa/bb/cc + * @param query 查询,例如a=1&b=2 + * @param fragment 标识符例如#后边的部分 + * @param charset 编码,用于URLEncode和URLDecode + * @return UrlBuilder + */ + public static UrlBuilder of(String scheme, String host, int port, UrlPath path, UrlQuery query, String fragment, Charset charset) { + return new UrlBuilder(scheme, host, port, path, query, fragment, charset); + } + + /** + * 创建空的UrlBuilder + * + * @return UrlBuilder + */ + public static UrlBuilder create() { + return new UrlBuilder(); + } + + /** + * 构造 + */ + public UrlBuilder() { + this.charset = CharsetUtil.CHARSET_UTF_8; + } + + /** + * 构造 + * + * @param scheme 协议,默认http + * @param host 主机,例如127.0.0.1 + * @param port 端口,-1表示默认端口 + * @param path 路径,例如/aa/bb/cc + * @param query 查询,例如a=1&b=2 + * @param fragment 标识符例如#后边的部分 + * @param charset 编码,用于URLEncode和URLDecode + */ + public UrlBuilder(String scheme, String host, int port, UrlPath path, UrlQuery query, String fragment, Charset charset) { + this.charset = charset; + this.scheme = scheme; + this.host = host; + this.port = port; + this.path = path; + this.query = query; + this.setFragment(fragment); + } + + /** + * 获取协议,例如http + * + * @return 协议,例如http + */ + public String getScheme() { + return scheme; + } + + /** + * 获取协议,例如http,如果用户未定义协议,使用默认的http协议 + * + * @return 协议,例如http + */ + public String getSchemeWithDefault() { + return StrUtil.emptyToDefault(this.scheme, DEFAULT_SCHEME); + } + + /** + * 设置协议,例如http + * + * @param scheme 协议,例如http + * @return this + */ + public UrlBuilder setScheme(String scheme) { + this.scheme = scheme; + return this; + } + + /** + * 获取 主机,例如127.0.0.1 + * + * @return 主机,例如127.0.0.1 + */ + public String getHost() { + return host; + } + + /** + * 设置主机,例如127.0.0.1 + * + * @param host 主机,例如127.0.0.1 + * @return this + */ + public UrlBuilder setHost(String host) { + this.host = host; + return this; + } + + /** + * 获取端口,默认-1 + * + * @return 端口,默认-1 + */ + public int getPort() { + return port; + } + + /** + * 设置端口,默认-1 + * + * @param port 端口,默认-1 + * @return this + */ + public UrlBuilder setPort(int port) { + this.port = port; + return this; + } + + /** + * 获得authority部分 + * + * @return authority部分 + */ + public String getAuthority() { + return (port < 0) ? host : host + ":" + port; + } + + /** + * 获取路径,例如/aa/bb/cc + * + * @return 路径,例如/aa/bb/cc + */ + public UrlPath getPath() { + return path; + } + + /** + * 获得路径,例如/aa/bb/cc + * + * @return 路径,例如/aa/bb/cc + */ + public String getPathStr() { + return null == this.path ? StrUtil.SLASH : this.path.build(charset); + } + + /** + * 设置路径,例如/aa/bb/cc,将覆盖之前所有的path相关设置 + * + * @param path 路径,例如/aa/bb/cc + * @return this + */ + public UrlBuilder setPath(UrlPath path) { + this.path = path; + return this; + } + + /** + * 增加路径节点 + * + * @param segment 路径节点 + * @return this + */ + public UrlBuilder addPath(String segment) { + if (StrUtil.isBlank(segment)) { + return this; + } + if (null == this.path) { + this.path = new UrlPath(); + } + this.path.add(segment); + return this; + } + + /** + * 追加path节点 + * + * @param segment path节点 + * @return this + */ + public UrlBuilder appendPath(CharSequence segment) { + if (StrUtil.isEmpty(segment)) { + return this; + } + + if (this.path == null) { + this.path = new UrlPath(); + } + this.path.add(segment); + return this; + } + + /** + * 获取查询语句,例如a=1&b=2 + * + * @return 查询语句,例如a=1&b=2 + */ + public UrlQuery getQuery() { + return query; + } + + /** + * 获取查询语句,例如a=1&b=2 + * + * @return 查询语句,例如a=1&b=2 + */ + public String getQueryStr() { + return null == this.query ? null : this.query.build(this.charset); + } + + /** + * 设置查询语句,例如a=1&b=2,将覆盖之前所有的query相关设置 + * + * @param query 查询语句,例如a=1&b=2 + * @return this + */ + public UrlBuilder setQuery(UrlQuery query) { + this.query = query; + return this; + } + + /** + * 添加查询项,支持重复键 + * + * @param key 键 + * @param value 值 + * @return this + */ + public UrlBuilder addQuery(String key, String value) { + if (StrUtil.isEmpty(key)) { + return this; + } + + if (this.query == null) { + this.query = new UrlQuery(); + } + this.query.add(key, value); + return this; + } + + /** + * 获取标识符,#后边的部分 + * + * @return 标识符,例如#后边的部分 + */ + public String getFragment() { + return fragment; + } + + /** + * 获取标识符,#后边的部分 + * + * @return 标识符,例如#后边的部分 + */ + public String getFragmentEncoded() { + return URLUtil.encodeAll(this.fragment, this.charset); + } + + /** + * 设置标识符,例如#后边的部分 + * + * @param fragment 标识符,例如#后边的部分 + * @return this + */ + public UrlBuilder setFragment(String fragment) { + if (StrUtil.isEmpty(fragment)) { + this.fragment = null; + } + this.fragment = StrUtil.removePrefix(fragment, "#"); + return this; + } + + /** + * 获取编码,用于URLEncode和URLDecode + * + * @return 编码 + */ + public Charset getCharset() { + return charset; + } + + /** + * 设置编码,用于URLEncode和URLDecode + * + * @param charset 编码 + * @return this + */ + public UrlBuilder setCharset(Charset charset) { + this.charset = charset; + return this; + } + + /** + * 创建URL字符串 + * + * @return URL字符串 + */ + public String build() { + return toURL().toString(); + } + + /** + * 转换为{@link URL} 对象 + * + * @return {@link URL} + */ + public URL toURL() { + return toURL(null); + } + + /** + * 转换为{@link URL} 对象 + * + * @param handler {@link URLStreamHandler},null表示默认 + * @return {@link URL} + */ + public URL toURL(URLStreamHandler handler) { + final StringBuilder fileBuilder = new StringBuilder(); + + // path + fileBuilder.append(StrUtil.blankToDefault(getPathStr(), StrUtil.SLASH)); + + // query + final String query = getQueryStr(); + if (StrUtil.isNotBlank(query)) { + fileBuilder.append('?').append(query); + } + + // fragment + if (StrUtil.isNotBlank(this.fragment)) { + fileBuilder.append('#').append(getFragmentEncoded()); + } + + try { + return new URL(getSchemeWithDefault(), host, port, fileBuilder.toString(), handler); + } catch (MalformedURLException e) { + return null; + } + } + + /** + * 转换为URI + * + * @return URI + */ + public URI toURI() { + try { + return new URI( + getSchemeWithDefault(), + getAuthority(), + getPathStr(), + getQueryStr(), + getFragmentEncoded()); + } catch (URISyntaxException e) { + return null; + } + } + + @Override + public String toString() { + return build(); + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..01708765a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java @@ -0,0 +1,172 @@ +package cn.hutool.core.net.url; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +import java.nio.charset.Charset; +import java.util.LinkedList; +import java.util.List; +import java.util.StringTokenizer; + +/** + * URL中Path部分的封装 + * + * @author looly + * @since 5.3.1 + */ +public class UrlPath { + private List segments; + private boolean withEngTag; + + /** + * 构建UrlPath + * + * @param pathStr 初始化的路径字符串 + * @param charset decode用的编码,null表示不做decode + * @return {@link UrlPath} + */ + public static UrlPath of(String pathStr, Charset charset) { + final UrlPath urlPath = new UrlPath(); + urlPath.parse(pathStr, charset); + return urlPath; + } + + /** + * 是否path的末尾加 / + * @param withEngTag 是否path的末尾加 / + * @return this + */ + public UrlPath setWithEndTag(boolean withEngTag){ + this.withEngTag = withEngTag; + return this; + } + + /** + * 获取path的节点列表 + * + * @return 节点列表 + */ + public List getSegments() { + return this.segments; + } + + /** + * 获得指定节点 + * + * @param index 节点位置 + * @return 节点,无节点或者越界返回null + */ + public String getSegment(int index) { + if (null == this.segments || index >= this.segments.size()) { + return null; + } + return this.segments.get(index); + } + + /** + * 添加到path最后面 + */ + public UrlPath add(CharSequence segment) { + add(segment, false); + return this; + } + + /** + * 添加到path最前面 + */ + public UrlPath addBefore(CharSequence segment) { + add(segment, true); + return this; + } + + /** + * 解析path + * + * @param path 路径,类似于aaa/bb/ccc + * @param charset decode编码,null表示不解码 + * @return this + */ + public UrlPath parse(String path, Charset charset) { + UrlPath urlPath = new UrlPath(); + + if (StrUtil.isNotEmpty(path)) { + path = path.trim(); + + final StringTokenizer tokenizer = new StringTokenizer(path, "/"); + while (tokenizer.hasMoreTokens()) { + add(URLUtil.decode(tokenizer.nextToken(), charset)); + } + } + + return urlPath; + } + + /** + * 构建path,前面带'/' + * + * @param charset encode编码,null表示不做encode + * @return 如果没有任何内容,则返回空字符串"" + */ + public String build(Charset charset) { + if (CollUtil.isEmpty(this.segments)) { + return StrUtil.EMPTY; + } + + final StringBuilder builder = new StringBuilder(); + for (String segment : segments) { + builder.append(CharUtil.SLASH).append(URLUtil.encodeAll(segment, charset)); + } + if(withEngTag || StrUtil.isEmpty(builder)){ + builder.append(CharUtil.SLASH); + } + return builder.toString(); + } + + @Override + public String toString() { + return build(null); + } + + /** + * 增加节点 + * + * @param segment 节点 + * @param before 是否在前面添加 + */ + private void add(CharSequence segment, boolean before) { + final String seg = fixSegment(segment); + if (null == seg) { + return; + } + + + if (this.segments == null) { + this.segments = new LinkedList<>(); + } + if (before) { + this.segments.add(0, seg); + } else { + this.segments.add(seg); + } + } + + /** + * 修正节点,包括去掉前后的/,去掉空白符 + * @param segment 节点 + * @return 修正后的节点 + */ + private static String fixSegment(CharSequence segment) { + if (StrUtil.isEmpty(segment) || "/".contentEquals(segment)) { + return null; + } + + String segmentStr = StrUtil.str(segment); + segmentStr = StrUtil.trim(segmentStr); + segmentStr = StrUtil.removePrefix(segmentStr, "/"); + segmentStr = StrUtil.removeSuffix(segmentStr, "/"); + segmentStr = StrUtil.trim(segmentStr); + return segmentStr; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/net/url/UrlQuery.java b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlQuery.java new file mode 100644 index 000000000..710cb9485 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlQuery.java @@ -0,0 +1,245 @@ +package cn.hutool.core.net.url; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.map.TableMap; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.Map; + +/** + * URL中查询字符串部分的封装,类似于: + *
+ *   key1=v1&key2=&key3=v3
+ * 
+ * + * @author looly + * @since 5.3.1 + */ +public class UrlQuery { + + private final TableMap query; + + /** + * 构建UrlQuery + * + * @param queryMap 初始化的查询键值对 + * @return {@link UrlQuery} + */ + public static UrlQuery of(Map queryMap) { + return new UrlQuery(queryMap); + } + + /** + * 构建UrlQuery + * + * @param queryStr 初始化的查询字符串 + * @param charset decode用的编码,null表示不做decode + * @return {@link UrlQuery} + */ + public static UrlQuery of(String queryStr, Charset charset) { + final UrlQuery urlQuery = new UrlQuery(); + urlQuery.parse(queryStr, charset); + return urlQuery; + } + + /** + * 构造 + */ + public UrlQuery() { + this(null); + } + + /** + * 构造 + * + * @param queryMap 初始化的查询键值对 + */ + public UrlQuery(Map queryMap) { + if(MapUtil.isNotEmpty(queryMap)) { + query = new TableMap<>(queryMap.size()); + addAll(queryMap); + } else{ + query = new TableMap<>(MapUtil.DEFAULT_INITIAL_CAPACITY); + } + } + + /** + * 增加键值对 + * + * @param key 键 + * @param value 值,集合和数组转换为逗号分隔形式 + * @return this + */ + public UrlQuery add(CharSequence key, Object value) { + this.query.put(key, toStr(value)); + return this; + } + + /** + * 批量增加键值对 + * + * @param queryMap query中的键值对 + * @return this + */ + public UrlQuery addAll(Map queryMap) { + if(MapUtil.isNotEmpty(queryMap)) { + queryMap.forEach(this::add); + } + return this; + } + + /** + * 解析URL中的查询字符串 + * + * @param queryStr 查询字符串,类似于key1=v1&key2=&key3=v3 + * @param charset decode编码,null表示不做decode + * @return this + */ + public UrlQuery parse(String queryStr, Charset charset) { + if (StrUtil.isBlank(queryStr)) { + return this; + } + + // 去掉Path部分 + int pathEndPos = queryStr.indexOf('?'); + if (pathEndPos > -1) { + queryStr = StrUtil.subSuf(queryStr, pathEndPos + 1); + if (StrUtil.isBlank(queryStr)) { + return this; + } + } + + 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); + if (c == '=') { // 键值对的分界点 + if (null == name) { + // name可以是"" + name = queryStr.substring(pos, i); + } + pos = i + 1; + } else if (c == '&') { // 参数对的分界点 + if (null == name && pos != i) { + // 对于像&a&这类无参数值的字符串,我们将name为a的值设为"" + addParam(queryStr.substring(pos, i), StrUtil.EMPTY, charset); + } else if (name != null) { + addParam(name, queryStr.substring(pos, i), charset); + name = null; + } + pos = i + 1; + } + } + + // 处理结尾 + if (pos != i) { + if (name == null) { + addParam(queryStr.substring(pos, i), StrUtil.EMPTY, charset); + } else { + addParam(name, queryStr.substring(pos, i), charset); + } + } else if (name != null) { + addParam(name, StrUtil.EMPTY, charset); + } + return this; + } + + /** + * 获得查询的Map + * + * @return 查询的Map,只读 + */ + public Map getQueryMap(){ + return MapUtil.unmodifiable(this.query); + } + + /** + * 获取查询值 + * @param key 键 + * @return 值 + */ + public CharSequence get(CharSequence key){ + if(MapUtil.isEmpty(this.query)){ + return null; + } + return this.query.get(key); + } + + /** + * 构建URL查询字符串,即将key-value键值对转换为key1=v1&key2=&key3=v3形式 + * + * @param charset encode编码,null表示不做encode编码 + * @return URL查询字符串 + */ + public String build(Charset charset) { + if (MapUtil.isEmpty(this.query)) { + return StrUtil.EMPTY; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + CharSequence key; + CharSequence value; + for (Map.Entry entry : this.query) { + if (isFirst) { + isFirst = false; + } else { + sb.append("&"); + } + key = entry.getKey(); + if (StrUtil.isNotEmpty(key)) { + sb.append(URLUtil.encodeAll(StrUtil.str(key), charset)).append("="); + value = entry.getValue(); + if (StrUtil.isNotEmpty(value)) { + sb.append(URLUtil.encodeAll(StrUtil.str(value), charset)); + } + } + } + return sb.toString(); + } + + @Override + public String toString() { + return build(null); + } + + /** + * 对象转换为字符串,用于URL的Query中 + * + * @param value 值 + * @return 字符串 + */ + private static String toStr(Object value) { + String result; + if (value instanceof Iterable) { + result = CollUtil.join((Iterable) value, ","); + } else if (value instanceof Iterator) { + result = IterUtil.join((Iterator) value, ","); + } else { + result = Convert.toStr(value); + } + return result; + } + + /** + * 将键值对加入到值为List类型的Map中 + * + * @param name key + * @param value value + * @param charset 编码 + */ + private void addParam(String name, String value, Charset charset) { + name = URLUtil.decode(name, charset); + value = URLUtil.decode(value, charset); + this.query.put(name, value); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/net/url/package-info.java b/hutool-core/src/main/java/cn/hutool/core/net/url/package-info.java new file mode 100644 index 000000000..1df7ac259 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/url/package-info.java @@ -0,0 +1,7 @@ +/** + * URL相关工具 + * + * @author looly + * @since 5.3.1 + */ +package cn.hutool.core.net.url; \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/util/CharUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/CharUtil.java index 4610c0877..f46e87e93 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/CharUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/CharUtil.java @@ -332,4 +332,15 @@ public class CharUtil { public static int getType(int c) { return Character.getType(c); } + + /** + * 获取给定字符的16进制数值 + * + * @param b 字符 + * @return 16进制字符 + * @since 5.3.1 + */ + public static int digit16(int b) { + return Character.digit(b, 16); + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java index e9b5b26b2..d2e89f18a 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java @@ -2207,7 +2207,7 @@ public class StrUtil { if (ignoreCase) { return str1.toString().equalsIgnoreCase(str2.toString()); } else { - return str1.equals(str2); + return str1.toString().contentEquals(str2); } } diff --git a/hutool-core/src/main/java/cn/hutool/core/util/URLUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/URLUtil.java index 56e4ea049..8a75e508a 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/URLUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/URLUtil.java @@ -6,7 +6,9 @@ import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.net.URLDecoder; import cn.hutool.core.net.URLEncoder; +import cn.hutool.core.net.url.UrlQuery; import java.io.BufferedReader; import java.io.File; @@ -18,44 +20,69 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.net.URLDecoder; import java.net.URLStreamHandler; import java.nio.charset.Charset; +import java.util.Map; import java.util.jar.JarFile; /** * 统一资源定位符相关工具类 * * @author xiaoleilu - * */ public class URLUtil { - /** 针对ClassPath路径的伪协议前缀(兼容Spring): "classpath:" */ + /** + * 针对ClassPath路径的伪协议前缀(兼容Spring): "classpath:" + */ public static final String CLASSPATH_URL_PREFIX = "classpath:"; - /** URL 前缀表示文件: "file:" */ + /** + * URL 前缀表示文件: "file:" + */ public static final String FILE_URL_PREFIX = "file:"; - /** URL 前缀表示jar: "jar:" */ + /** + * URL 前缀表示jar: "jar:" + */ public static final String JAR_URL_PREFIX = "jar:"; - /** URL 前缀表示war: "war:" */ + /** + * URL 前缀表示war: "war:" + */ public static final String WAR_URL_PREFIX = "war:"; - /** URL 协议表示文件: "file" */ + /** + * URL 协议表示文件: "file" + */ public static final String URL_PROTOCOL_FILE = "file"; - /** URL 协议表示Jar文件: "jar" */ + /** + * URL 协议表示Jar文件: "jar" + */ public static final String URL_PROTOCOL_JAR = "jar"; - /** URL 协议表示zip文件: "zip" */ + /** + * URL 协议表示zip文件: "zip" + */ public static final String URL_PROTOCOL_ZIP = "zip"; - /** URL 协议表示WebSphere文件: "wsjar" */ + /** + * URL 协议表示WebSphere文件: "wsjar" + */ public static final String URL_PROTOCOL_WSJAR = "wsjar"; - /** URL 协议表示JBoss zip文件: "vfszip" */ + /** + * URL 协议表示JBoss zip文件: "vfszip" + */ public static final String URL_PROTOCOL_VFSZIP = "vfszip"; - /** URL 协议表示JBoss文件: "vfsfile" */ + /** + * URL 协议表示JBoss文件: "vfsfile" + */ public static final String URL_PROTOCOL_VFSFILE = "vfsfile"; - /** URL 协议表示JBoss VFS资源: "vfs" */ + /** + * URL 协议表示JBoss VFS资源: "vfs" + */ public static final String URL_PROTOCOL_VFS = "vfs"; - /** Jar路径以及内部文件路径的分界符: "!/" */ + /** + * Jar路径以及内部文件路径的分界符: "!/" + */ public static final String JAR_URL_SEPARATOR = "!/"; - /** WAR路径及内部文件路径分界符 */ + /** + * WAR路径及内部文件路径分界符 + */ public static final String WAR_URL_SEPARATOR = "*/"; /** @@ -71,7 +98,7 @@ public class URLUtil { /** * 通过一个字符串形式的URL地址创建URL对象 * - * @param url URL + * @param url URL * @param handler {@link URLStreamHandler} * @return URL对象 * @since 4.1.1 @@ -111,7 +138,7 @@ public class URLUtil { /** * 将URL字符串转换为URL对象,并做必要验证 * - * @param urlStr URL字符串 + * @param urlStr URL字符串 * @param handler {@link URLStreamHandler} * @return URL * @since 4.1.9 @@ -167,7 +194,7 @@ public class URLUtil { /** * 获得URL * - * @param path 相对给定 class所在的路径 + * @param path 相对给定 class所在的路径 * @param clazz 指定class * @return URL * @see ResourceUtil#getResource(String, Class) @@ -181,7 +208,7 @@ public class URLUtil { * * @param file URL对应的文件对象 * @return URL - * @exception UtilException MalformedURLException + * @throws UtilException MalformedURLException */ public static URL getURL(File file) { Assert.notNull(file, "File is null !"); @@ -197,7 +224,7 @@ public class URLUtil { * * @param files URL对应的文件对象 * @return URL - * @exception UtilException MalformedURLException + * @throws UtilException MalformedURLException */ public static URL[] getURLs(File... files) { final URL[] urls = new URL[files.length]; @@ -219,8 +246,8 @@ public class URLUtil { * @return 域名的URI * @since 4.6.9 */ - public static URI getHost(URL url){ - if(null == url){ + public static URI getHost(URL url) { + if (null == url) { return null; } @@ -234,12 +261,26 @@ public class URLUtil { /** * 补全相对路径 * - * @param baseUrl 基准URL + * @param baseUrl 基准URL * @param relativePath 相对URL * @return 相对路径 - * @exception UtilException MalformedURLException + * @throws UtilException MalformedURLException + * @deprecated 拼写错误,请使用{@link #completeUrl(String, String)} */ + @Deprecated public static String complateUrl(String baseUrl, String relativePath) { + return completeUrl(baseUrl, relativePath); + } + + /** + * 补全相对路径 + * + * @param baseUrl 基准URL + * @param relativePath 相对URL + * @return 相对路径 + * @throws UtilException MalformedURLException + */ + public static String completeUrl(String baseUrl, String relativePath) { baseUrl = normalize(baseUrl, false); if (StrUtil.isBlank(baseUrl)) { return null; @@ -260,7 +301,7 @@ public class URLUtil { * * @param url URL * @return 编码后的URL - * @exception UtilException UnsupportedEncodingException + * @throws UtilException UnsupportedEncodingException */ public static String encodeAll(String url) { return encodeAll(url, CharsetUtil.CHARSET_UTF_8); @@ -270,12 +311,15 @@ public class URLUtil { * 编码URL
* 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。 * - * @param url URL - * @param charset 编码 + * @param url URL + * @param charset 编码,为null表示不编码 * @return 编码后的URL - * @exception UtilException UnsupportedEncodingException + * @throws UtilException UnsupportedEncodingException */ public static String encodeAll(String url, Charset charset) throws UtilException { + if (null == charset) { + return url; + } try { return java.net.URLEncoder.encode(url, charset.toString()); } catch (UnsupportedEncodingException e) { @@ -290,7 +334,7 @@ public class URLUtil { * * @param url URL * @return 编码后的URL - * @exception UtilException UnsupportedEncodingException + * @throws UtilException UnsupportedEncodingException * @since 3.1.2 */ public static String encode(String url) throws UtilException { @@ -304,7 +348,7 @@ public class URLUtil { * * @param url URL * @return 编码后的URL - * @exception UtilException UnsupportedEncodingException + * @throws UtilException UnsupportedEncodingException * @since 3.1.2 */ public static String encodeQuery(String url) throws UtilException { @@ -316,7 +360,7 @@ public class URLUtil { * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
* 此方法用于URL自动编码,类似于浏览器中键入地址自动编码,对于像类似于“/”的字符不再编码 * - * @param url 被编码内容 + * @param url 被编码内容 * @param charset 编码 * @return 编码后的字符 * @since 4.4.1 @@ -336,7 +380,7 @@ public class URLUtil { * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
* 此方法用于POST请求中的请求体自动编码,转义大部分特殊字符 * - * @param url 被编码内容 + * @param url 被编码内容 * @param charset 编码 * @return 编码后的字符 * @since 4.4.1 @@ -356,10 +400,10 @@ public class URLUtil { * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
* 此方法用于URL自动编码,类似于浏览器中键入地址自动编码,对于像类似于“/”的字符不再编码 * - * @param url URL + * @param url URL * @param charset 编码 * @return 编码后的URL - * @exception UtilException UnsupportedEncodingException + * @throws UtilException UnsupportedEncodingException */ public static String encode(String url, String charset) throws UtilException { if (StrUtil.isEmpty(url)) { @@ -373,10 +417,10 @@ public class URLUtil { * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
* 此方法用于POST请求中的请求体自动编码,转义大部分特殊字符 * - * @param url URL + * @param url URL * @param charset 编码 * @return 编码后的URL - * @exception UtilException UnsupportedEncodingException + * @throws UtilException UnsupportedEncodingException */ public static String encodeQuery(String url, String charset) throws UtilException { return encodeQuery(url, StrUtil.isBlank(charset) ? CharsetUtil.defaultCharset() : CharsetUtil.charset(charset)); @@ -388,7 +432,7 @@ public class URLUtil { * * @param url URL * @return 解码后的URL - * @exception UtilException UnsupportedEncodingException + * @throws UtilException UnsupportedEncodingException * @since 3.1.2 */ public static String decode(String url) throws UtilException { @@ -396,38 +440,32 @@ public class URLUtil { } /** - * 解码application/x-www-form-urlencoded字符 + * 解码application/x-www-form-urlencoded字符
+ * 将%开头的16进制表示的内容解码。 * * @param content 被解码内容 - * @param charset 编码 + * @param charset 编码,null表示不解码 * @return 编码后的字符 * @since 4.4.1 */ public static String decode(String content, Charset charset) { if (null == charset) { - charset = CharsetUtil.defaultCharset(); + return content; } - return decode(content, charset.name()); + return URLDecoder.decode(content, charset); } /** - * 解码URL
+ * 解码application/x-www-form-urlencoded字符
* 将%开头的16进制表示的内容解码。 * - * @param url URL + * @param content URL * @param charset 编码 * @return 解码后的URL - * @exception UtilException UnsupportedEncodingException + * @throws UtilException UnsupportedEncodingException */ - public static String decode(String url, String charset) throws UtilException { - if (StrUtil.isEmpty(url)) { - return url; - } - try { - return URLDecoder.decode(url, charset); - } catch (UnsupportedEncodingException e) { - throw new UtilException(e, "Unsupported encoding: [{}]", charset); - } + public static String decode(String content, String charset) throws UtilException { + return decode(content, CharsetUtil.charset(charset)); } /** @@ -435,7 +473,7 @@ public class URLUtil { * * @param uriStr URI路径 * @return path - * @exception UtilException 包装URISyntaxException + * @throws UtilException 包装URISyntaxException */ public static String getPath(String uriStr) { URI uri; @@ -476,7 +514,7 @@ public class URLUtil { * * @param url URL * @return URI - * @exception UtilException 包装URISyntaxException + * @throws UtilException 包装URISyntaxException */ public static URI toURI(URL url) throws UtilException { return toURI(url, false); @@ -485,10 +523,10 @@ public class URLUtil { /** * 转URL为URI * - * @param url URL + * @param url URL * @param isEncode 是否编码参数中的特殊字符(默认UTF-8编码) * @return URI - * @exception UtilException 包装URISyntaxException + * @throws UtilException 包装URISyntaxException * @since 4.6.9 */ public static URI toURI(URL url, boolean isEncode) throws UtilException { @@ -504,7 +542,7 @@ public class URLUtil { * * @param location 字符串路径 * @return URI - * @exception UtilException 包装URISyntaxException + * @throws UtilException 包装URISyntaxException */ public static URI toURI(String location) throws UtilException { return toURI(location, false); @@ -516,11 +554,11 @@ public class URLUtil { * @param location 字符串路径 * @param isEncode 是否编码参数中的特殊字符(默认UTF-8编码) * @return URI - * @exception UtilException 包装URISyntaxException + * @throws UtilException 包装URISyntaxException * @since 4.6.9 */ public static URI toURI(String location, boolean isEncode) throws UtilException { - if(isEncode){ + if (isEncode) { location = encode(location); } try { @@ -590,7 +628,7 @@ public class URLUtil { /** * 获得Reader * - * @param url {@link URL} + * @param url {@link URL} * @param charset 编码 * @return {@link BufferedReader} * @since 3.2.1 @@ -636,7 +674,7 @@ public class URLUtil { * 1. 多个/替换为一个 * * - * @param url URL字符串 + * @param url URL字符串 * @param isEncodePath 是否对URL中path部分的中文和特殊字符做转义(不包括 http:, /和域名部分) * @return 标准化后的URL字符串 * @since 4.4.1 @@ -663,7 +701,7 @@ public class URLUtil { body = StrUtil.subPre(body, paramsSepIndex); } - if(StrUtil.isNotEmpty(body)){ + if (StrUtil.isNotEmpty(body)) { // 去除开头的\或者/ //noinspection ConstantConditions body = body.replaceAll("^[\\\\/]+", StrUtil.EMPTY); @@ -683,4 +721,21 @@ public class URLUtil { } return protocol + domain + StrUtil.nullToEmpty(path) + StrUtil.nullToEmpty(params); } + + /** + * 将Map形式的Form表单数据转换为Url参数形式
+ * paramMap中如果key为空(null和"")会被忽略,如果value为null,会被做为空白符("")
+ * 会自动url编码键和值 + * + *
+	 * key1=v1&key2=&key3=v3
+	 * 
+ * + * @param paramMap 表单数据 + * @param charset 编码,编码为null表示不编码 + * @return url参数 + */ + public static String buildQuery(Map paramMap, Charset charset) { + return UrlQuery.of(paramMap).build(charset); + } } \ No newline at end of file diff --git a/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java index b1fa0d05d..360051230 100644 --- a/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java @@ -4,7 +4,6 @@ import cn.hutool.core.annotation.Alias; import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.bean.copier.ValueProvider; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Console; import cn.hutool.core.map.MapUtil; import lombok.Getter; import lombok.Setter; @@ -154,7 +153,6 @@ public class BeanUtilTest { person.setSlow(true); Map map = BeanUtil.beanToMap(person); - Console.log(map); Assert.assertEquals("sub名字", map.get("aliasSubName")); } 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 new file mode 100644 index 000000000..777169541 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java @@ -0,0 +1,173 @@ +package cn.hutool.core.net; + +import cn.hutool.core.net.url.UrlBuilder; +import cn.hutool.core.util.CharsetUtil; +import org.junit.Assert; +import org.junit.Test; + +public class UrlBuilderTest { + + @Test + public void buildTest() { + String buildUrl = UrlBuilder.create().setHost("www.baidu.com").build(); + Assert.assertEquals("http://www.baidu.com/", buildUrl); + } + + @Test + public void testHost() { + String buildUrl = UrlBuilder.create() + .setScheme("https") + .setHost("www.baidu.com").build(); + Assert.assertEquals("https://www.baidu.com/", buildUrl); + } + + @Test + public void testHostPort() { + String buildUrl = UrlBuilder.create() + .setScheme("https") + .setHost("www.baidu.com") + .setPort(8080) + .build(); + Assert.assertEquals("https://www.baidu.com:8080/", buildUrl); + } + + @Test + public void testPathAndQuery() { + final String buildUrl = UrlBuilder.create() + .setScheme("https") + .setHost("www.baidu.com") + .addPath("/aaa").addPath("bbb") + .addQuery("ie", "UTF-8") + .addQuery("wd", "test") + .build(); + + Assert.assertEquals("https://www.baidu.com/aaa/bbb?ie=UTF-8&wd=test", buildUrl); + } + + @Test + public void testQueryWithChinese() { + final String buildUrl = UrlBuilder.create() + .setScheme("https") + .setHost("www.baidu.com") + .addPath("/aaa").addPath("bbb") + .addQuery("ie", "UTF-8") + .addQuery("wd", "测试") + .build(); + + Assert.assertEquals("https://www.baidu.com/aaa/bbb?ie=UTF-8&wd=%E6%B5%8B%E8%AF%95", buildUrl); + } + + @Test + public void testMultiQueryWithChinese() { + final String buildUrl = UrlBuilder.create() + .setScheme("https") + .setHost("www.baidu.com") + .addPath("/s") + .addQuery("ie", "UTF-8") + .addQuery("ie", "GBK") + .addQuery("wd", "测试") + .build(); + + Assert.assertEquals("https://www.baidu.com/s?ie=UTF-8&ie=GBK&wd=%E6%B5%8B%E8%AF%95", buildUrl); + } + + @Test + public void testFragment() { + String buildUrl = new UrlBuilder() + .setScheme("https") + .setHost("www.baidu.com") + .setFragment("abc").build(); + Assert.assertEquals("https://www.baidu.com/#abc", buildUrl); + } + + @Test + public void testChineseFragment() { + String buildUrl = new UrlBuilder() + .setScheme("https") + .setHost("www.baidu.com") + .setFragment("测试").build(); + Assert.assertEquals("https://www.baidu.com/#%E6%B5%8B%E8%AF%95", buildUrl); + } + + @Test + public void testChineseFragmentWithPath() { + String buildUrl = new UrlBuilder() + .setScheme("https") + .setHost("www.baidu.com") + .addPath("/s") + .setFragment("测试").build(); + Assert.assertEquals("https://www.baidu.com/s#%E6%B5%8B%E8%AF%95", buildUrl); + } + + @Test + public void testChineseFragmentWithPathAndQuery() { + String buildUrl = new UrlBuilder() + .setScheme("https") + .setHost("www.baidu.com") + .addPath("/s") + .addQuery("wd", "test") + .setFragment("测试").build(); + Assert.assertEquals("https://www.baidu.com/s?wd=test#%E6%B5%8B%E8%AF%95", buildUrl); + } + + @Test + public void ofTest() { + final UrlBuilder builder = UrlBuilder.of("http://www.baidu.com/aaa/bbb/?a=1&b=2#frag1", CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("http", builder.getScheme()); + Assert.assertEquals("www.baidu.com", builder.getHost()); + + Assert.assertEquals("aaa", builder.getPath().getSegment(0)); + Assert.assertEquals("bbb", builder.getPath().getSegment(1)); + + Assert.assertEquals("1", builder.getQuery().get("a")); + Assert.assertEquals("2", builder.getQuery().get("b")); + + Assert.assertEquals("frag1", builder.getFragment()); + } + + @Test + public void ofWithChineseTest() { + final UrlBuilder builder = UrlBuilder.ofHttp("www.baidu.com/aaa/bbb/?a=张三&b=%e6%9d%8e%e5%9b%9b#frag1", CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("http", builder.getScheme()); + Assert.assertEquals("www.baidu.com", builder.getHost()); + + Assert.assertEquals("aaa", builder.getPath().getSegment(0)); + Assert.assertEquals("bbb", builder.getPath().getSegment(1)); + + Assert.assertEquals("张三", builder.getQuery().get("a")); + Assert.assertEquals("李四", builder.getQuery().get("b")); + + Assert.assertEquals("frag1", builder.getFragment()); + } + + @Test + public void ofWithBlankTest() { + final UrlBuilder builder = UrlBuilder.ofHttp(" www.baidu.com/aaa/bbb/?a=张三&b=%e6%9d%8e%e5%9b%9b#frag1", CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("http", builder.getScheme()); + Assert.assertEquals("www.baidu.com", builder.getHost()); + + Assert.assertEquals("aaa", builder.getPath().getSegment(0)); + Assert.assertEquals("bbb", builder.getPath().getSegment(1)); + + Assert.assertEquals("张三", builder.getQuery().get("a")); + Assert.assertEquals("李四", builder.getQuery().get("b")); + + Assert.assertEquals("frag1", builder.getFragment()); + } + + @Test + public void ofSpecialTest() { + //测试不规范的或者无需解码的字符串是否成功解码 + final UrlBuilder builder = UrlBuilder.ofHttp(" www.baidu.com/aaa/bbb/?a=张三&b=%%e5%9b%9b#frag1", CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("http", builder.getScheme()); + Assert.assertEquals("www.baidu.com", builder.getHost()); + + Assert.assertEquals("aaa", builder.getPath().getSegment(0)); + Assert.assertEquals("bbb", builder.getPath().getSegment(1)); + + Assert.assertEquals("张三", builder.getQuery().get("a")); + Assert.assertEquals("%四", builder.getQuery().get("b")); + + Assert.assertEquals("frag1", builder.getFragment()); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/AES.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/AES.java index 758bd7838..4b34b67e1 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/AES.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/AES.java @@ -1,14 +1,14 @@ package cn.hutool.crypto.symmetric; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; - import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.SecureUtil; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + /** * AES加密算法实现
* 高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法
@@ -105,7 +105,7 @@ public class AES extends SymmetricCrypto { * @since 4.6.7 */ public AES(Mode mode, Padding padding, SecretKey key, byte[] iv) { - this(mode, padding, key, ArrayUtil.isEmpty(iv) ? ((IvParameterSpec) null) : new IvParameterSpec(iv)); + this(mode, padding, key, ArrayUtil.isEmpty(iv) ? null : new IvParameterSpec(iv)); } /** @@ -153,7 +153,7 @@ public class AES extends SymmetricCrypto { public AES(String mode, String padding, byte[] key, byte[] iv) { this(mode, padding,// SecureUtil.generateKey(SymmetricAlgorithm.AES.getValue(), key),// - ArrayUtil.isEmpty(iv) ? ((IvParameterSpec) null) : new IvParameterSpec(iv)); + ArrayUtil.isEmpty(iv) ? null : new IvParameterSpec(iv)); } /** diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java index d8ffbdbfd..462df202f 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java @@ -12,11 +12,11 @@ import cn.hutool.core.io.resource.MultiResource; import cn.hutool.core.io.resource.Resource; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.core.util.URLUtil; import cn.hutool.http.cookie.GlobalCookieManager; import cn.hutool.http.ssl.SSLSocketFactoryBuilder; @@ -96,7 +96,7 @@ public class HttpRequest extends HttpBase { GlobalCookieManager.setCookieManager(null); } - private String url; + private UrlBuilder url; private URLStreamHandler urlHandler; private Method method = Method.GET; /** @@ -128,10 +128,6 @@ public class HttpRequest extends HttpBase { * 是否禁用缓存 */ private boolean isDisableCache; - /** - * 是否对url中的参数进行编码 - */ - private boolean encodeUrlParams; /** * 是否是REST请求模式 */ @@ -168,8 +164,7 @@ public class HttpRequest extends HttpBase { * @param url URL */ public HttpRequest(String url) { - Assert.notBlank(url, "Param [url] can not be blank !"); - this.url = URLUtil.normalize(url, true); + setUrl(url); // 给定一个默认头信息 this.header(GlobalHeaders.INSTANCE.headers); } @@ -265,7 +260,7 @@ public class HttpRequest extends HttpBase { * @since 4.1.8 */ public String getUrl() { - return url; + return url.toString(); } /** @@ -276,7 +271,19 @@ public class HttpRequest extends HttpBase { * @since 4.1.8 */ public HttpRequest setUrl(String url) { - this.url = url; + this.url = UrlBuilder.ofHttp(url, this.charset); + return this; + } + + /** + * 设置URL + * + * @param urlBuilder url字符串 + * @return this + * @since 5.3.1 + */ + public HttpRequest setUrl(UrlBuilder urlBuilder) { + this.url = urlBuilder; return this; } @@ -774,9 +781,10 @@ public class HttpRequest extends HttpBase { * @param isEncodeUrlParams 是否对URL中的参数进行编码 * @return this * @since 4.4.1 + * @deprecated 编码自动完成,无需设置 */ + @Deprecated public HttpRequest setEncodeUrlParams(boolean isEncodeUrlParams) { - this.encodeUrlParams = isEncodeUrlParams; return this; } @@ -925,10 +933,6 @@ public class HttpRequest extends HttpBase { public HttpResponse execute(boolean isAsync) { // 初始化URL urlWithParamIfGet(); - // 编码URL - if (this.encodeUrlParams) { - this.url = HttpUtil.encodeParams(this.url, this.charset); - } // 初始化 connection initConnection(); @@ -982,7 +986,8 @@ public class HttpRequest extends HttpBase { this.httpConnection.disconnectQuietly(); } - this.httpConnection = HttpConnection.create(URLUtil.toUrlForHttp(this.url, this.urlHandler), this.proxy)// + this.httpConnection = HttpConnection + .create(this.url.toURL(this.urlHandler), this.proxy)// .setMethod(this.method)// .setHttpsInfo(this.hostnameVerifier, this.ssf)// .setConnectTimeout(this.connectionTimeout)// @@ -1016,9 +1021,9 @@ public class HttpRequest extends HttpBase { if (Method.GET.equals(method) && false == this.isRest) { // 优先使用body形式的参数,不存在使用form if (ArrayUtil.isNotEmpty(this.bodyBytes)) { - this.url = HttpUtil.urlWithForm(this.url, StrUtil.str(this.bodyBytes, this.charset), this.charset, false); + this.url.getQuery().parse(StrUtil.str(this.bodyBytes, this.charset), this.charset); } else { - this.url = HttpUtil.urlWithForm(this.url, this.form, this.charset, false); + this.url.getQuery().addAll(this.form); } } } @@ -1047,7 +1052,7 @@ public class HttpRequest extends HttpBase { if (responseCode != HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_SEE_OTHER) { - this.url = httpConnection.header(Header.LOCATION); + setUrl(httpConnection.header(Header.LOCATION)); if (redirectCount < this.maxRedirectCount) { redirectCount++; return execute(); diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java index dea5e40d7..8d5aa6bfa 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java @@ -471,11 +471,11 @@ public class HttpResponse extends HttpBase implements Closeable { */ private String getFileNameFromDisposition() { String fileName = null; - final String desposition = header(Header.CONTENT_DISPOSITION); - if (StrUtil.isNotBlank(desposition)) { - fileName = ReUtil.get("filename=\"(.*?)\"", desposition, 1); + final String disposition = header(Header.CONTENT_DISPOSITION); + if (StrUtil.isNotBlank(disposition)) { + fileName = ReUtil.get("filename=\"(.*?)\"", disposition, 1); if (StrUtil.isBlank(fileName)) { - fileName = StrUtil.subAfter(desposition, "filename=", true); + fileName = StrUtil.subAfter(disposition, "filename=", true); } } return fileName; diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java index 07bb0b8af..c24bf6e6b 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java @@ -1,14 +1,12 @@ package cn.hutool.http; -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.collection.CollectionUtil; -import cn.hutool.core.collection.IterUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.io.FastByteArrayOutputStream; 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.url.UrlQuery; import cn.hutool.core.text.StrBuilder; import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.ObjectUtil; @@ -23,13 +21,9 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.regex.Pattern; /** @@ -390,44 +384,11 @@ public class HttpUtil { * * * @param paramMap 表单数据 - * @param charset 编码 + * @param charset 编码,null表示不encode键值对 * @return url参数 */ public static String toParams(Map paramMap, Charset charset) { - if (CollectionUtil.isEmpty(paramMap)) { - return StrUtil.EMPTY; - } - if (null == charset) {// 默认编码为系统编码 - charset = CharsetUtil.CHARSET_UTF_8; - } - - final StringBuilder sb = new StringBuilder(); - boolean isFirst = true; - String key; - Object value; - String valueStr; - for (Entry item : paramMap.entrySet()) { - if (isFirst) { - isFirst = false; - } else { - sb.append("&"); - } - key = item.getKey(); - value = item.getValue(); - if (value instanceof Iterable) { - value = CollUtil.join((Iterable) value, ","); - } else if (value instanceof Iterator) { - value = IterUtil.join((Iterator) value, ","); - } - valueStr = Convert.toStr(value); - if (StrUtil.isNotEmpty(key)) { - sb.append(URLUtil.encodeAll(key, charset)).append("="); - if (StrUtil.isNotEmpty(valueStr)) { - sb.append(URLUtil.encodeAll(valueStr, charset)); - } - } - } - return sb.toString(); + return URLUtil.buildQuery(paramMap, charset); } /** @@ -437,7 +398,7 @@ public class HttpUtil { *

注意,此方法只能标准化整个URL,并不适合于单独编码参数值

* * @param urlWithParams url和参数,可以包含url本身,也可以单独参数 - * @param charset 编码 + * @param charset 编码 * @return 编码后的url和参数 * @since 4.0.1 */ @@ -457,10 +418,10 @@ public class HttpUtil { // 无参数,返回url return urlPart; } - } else if(false == StrUtil.contains(urlWithParams, '=')){ + } else if (false == StrUtil.contains(urlWithParams, '=')) { // 无参数的URL return urlWithParams; - }else { + } else { // 无URL的参数 paramPart = urlWithParams; } @@ -536,8 +497,10 @@ public class HttpUtil { * @param charset 字符集 * @return 参数Map * @since 4.0.2 + * @deprecated 请使用 {@link #decodeParamMap(String, Charset)} */ - public static HashMap decodeParamMap(String paramsStr, String charset) { + @Deprecated + public static Map decodeParamMap(String paramsStr, String charset) { return decodeParamMap(paramsStr, CharsetUtil.charset(charset)); } @@ -549,15 +512,12 @@ public class HttpUtil { * @return 参数Map * @since 5.2.6 */ - public static HashMap decodeParamMap(String paramsStr, Charset charset) { - final Map> paramsMap = decodeParams(paramsStr, charset); - final HashMap result = MapUtil.newHashMap(paramsMap.size()); - List valueList; - for (Entry> entry : paramsMap.entrySet()) { - valueList = entry.getValue(); - result.put(entry.getKey(), CollUtil.isEmpty(valueList) ? null : valueList.get(0)); + public static Map decodeParamMap(String paramsStr, Charset charset) { + final Map queryMap = UrlQuery.of(paramsStr, charset).getQueryMap(); + if (MapUtil.isEmpty(queryMap)) { + return MapUtil.empty(); } - return result; + return Convert.toMap(String.class, String.class, queryMap); } /** @@ -580,56 +540,17 @@ public class HttpUtil { * @since 5.2.6 */ public static Map> decodeParams(String paramsStr, Charset charset) { - if (StrUtil.isBlank(paramsStr)) { - return Collections.emptyMap(); + final Map queryMap = UrlQuery.of(paramsStr, charset).getQueryMap(); + if (MapUtil.isEmpty(queryMap)) { + return MapUtil.empty(); } - // 去掉Path部分 - int pathEndPos = paramsStr.indexOf('?'); - if (pathEndPos > -1) { - paramsStr = StrUtil.subSuf(paramsStr, pathEndPos + 1); - if (StrUtil.isBlank(paramsStr)) { - return Collections.emptyMap(); - } - } - - final int len = paramsStr.length(); final Map> params = new LinkedHashMap<>(); - String name = null; - int pos = 0; // 未处理字符开始位置 - int i; // 未处理字符结束位置 - char c; // 当前字符 - for (i = 0; i < len; i++) { - c = paramsStr.charAt(i); - if (c == '=') { // 键值对的分界点 - if (null == name) { - // name可以是"" - name = paramsStr.substring(pos, i); - } - pos = i + 1; - } else if (c == '&') { // 参数对的分界点 - if (null == name && pos != i) { - // 对于像&a&这类无参数值的字符串,我们将name为a的值设为"" - addParam(params, paramsStr.substring(pos, i), StrUtil.EMPTY, charset); - } else if (name != null) { - addParam(params, name, paramsStr.substring(pos, i), charset); - name = null; - } - pos = i + 1; - } - } - - // 处理结尾 - if (pos != i) { - if (name == null) { - addParam(params, paramsStr.substring(pos, i), StrUtil.EMPTY, charset); - } else { - addParam(params, name, paramsStr.substring(pos, i), charset); - } - } else if (name != null) { - addParam(params, name, StrUtil.EMPTY, charset); - } - + queryMap.forEach((key, value) -> { + final List values = params.computeIfAbsent(StrUtil.str(key), k -> new ArrayList<>(1)); + // 一般是一个参数 + values.add(StrUtil.str(value)); + }); return params; } @@ -826,26 +747,7 @@ public class HttpUtil { * @return {@link SimpleServer} * @since 5.2.6 */ - public static SimpleServer createServer(int port){ + public static SimpleServer createServer(int port) { return new SimpleServer(port); } - // ----------------------------------------------------------------------------------------- Private method start - - /** - * 将键值对加入到值为List类型的Map中 - * - * @param params 参数 - * @param name key - * @param value value - * @param charset 编码 - */ - private static void addParam(Map> params, String name, String value, Charset charset) { - name = URLUtil.decode(name, charset); - value = URLUtil.decode(value, charset); - final List values = params.computeIfAbsent(name, k -> new ArrayList<>(1)); - // 一般是一个参数 - values.add(value); - } - - // ----------------------------------------------------------------------------------------- Private method start end } diff --git a/hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java b/hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java index 59afded99..fda2003d1 100644 --- a/hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java @@ -11,6 +11,7 @@ import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; +import java.io.ByteArrayOutputStream; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -18,7 +19,7 @@ import java.util.Map; public class HttpUtilTest { @Test - @Ignore +// @Ignore public void postTest() { String result = HttpUtil.createPost("api.uhaozu.com/goods/description/1120448506").charset(CharsetUtil.UTF_8).execute().body(); Console.log(result); @@ -71,6 +72,17 @@ public class HttpUtilTest { Console.log(str); } + @Test + @Ignore + public void getTest5() { + String url2 = "http://storage.chancecloud.com.cn/20200413_%E7%B2%A4B12313_386.pdf"; + ByteArrayOutputStream os2 = new ByteArrayOutputStream(); + HttpUtil.download(url2, os2, false); + + url2 = "http://storage.chancecloud.com.cn/20200413_粤B12313_386.pdf"; + HttpUtil.download(url2, os2, false); + } + @Test @Ignore public void get12306Test() {