add UrlDecoder

This commit is contained in:
Looly
2020-04-16 01:13:58 +08:00
parent 8102b31373
commit ca7c407a1c
18 changed files with 1402 additions and 229 deletions

View File

@@ -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 <V> 值类型
* @author looly
*/
public class TableMap<K, V> implements Map<K, V>, Serializable {
public class TableMap<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>>, Serializable {
private static final long serialVersionUID = 1L;
private final List<K> keys;
@@ -58,7 +59,7 @@ public class TableMap<K, V> implements Map<K, V>, Serializable {
@Override
public boolean isEmpty() {
return ArrayUtil.isEmpty(keys);
return CollUtil.isEmpty(keys);
}
@Override
@@ -159,13 +160,37 @@ public class TableMap<K, V> implements Map<K, V>, Serializable {
@SuppressWarnings("NullableProblems")
@Override
public Set<Map.Entry<K, V>> entrySet() {
HashSet<Map.Entry<K, V>> hashSet = new HashSet<>();
final Set<Map.Entry<K, V>> 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<Map.Entry<K, V>> iterator() {
return new Iterator<Map.Entry<K, V>>() {
private final Iterator<K> keysIter = keys.iterator();
private final Iterator<V> valuesIter = values.iterator();
@Override
public boolean hasNext() {
return keysIter.hasNext() && valuesIter.hasNext();
}
@Override
public Map.Entry<K, V> next() {
return new Entry<>(keysIter.next(), valuesIter.next());
}
@Override
public void remove() {
keysIter.remove();
valuesIter.remove();
}
};
}
private static class Entry<K, V> implements Map.Entry<K, V> {
private final K key;

View File

@@ -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。
*
* <pre>
* 1. 将%20转换为空格 ;
* 2. 将"%xy"转换为文本形式,xy是两位16进制的数值;
* 3. 跳过不符合规范的%形式,直接输出
* </pre>
*
* @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();
}
}

View File

@@ -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 对之间放置 &amp; 符号。
* </pre>
*
* @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);
}

View File

@@ -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 生成器,格式形如:
* <pre>
* [scheme:]scheme-specific-part[#fragment]
* [scheme:][//authority][path][?query][#fragment]
* [scheme:][//host:port][path][?query][#fragment]
* </pre>
*
* @author looly
* @see <a href="https://en.wikipedia.org/wiki/Uniform_Resource_Identifier">Uniform Resource Identifier</a>
* @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();
}
}

View File

@@ -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<String> 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<String> 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;
}
}

View File

@@ -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中查询字符串部分的封装类似于
* <pre>
* key1=v1&amp;key2=&amp;key3=v3
* </pre>
*
* @author looly
* @since 5.3.1
*/
public class UrlQuery {
private final TableMap<CharSequence, CharSequence> query;
/**
* 构建UrlQuery
*
* @param queryMap 初始化的查询键值对
* @return {@link UrlQuery}
*/
public static UrlQuery of(Map<? extends CharSequence, ?> 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<? extends CharSequence, ?> 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<? extends CharSequence, ?> queryMap) {
if(MapUtil.isNotEmpty(queryMap)) {
queryMap.forEach(this::add);
}
return this;
}
/**
* 解析URL中的查询字符串
*
* @param queryStr 查询字符串类似于key1=v1&amp;key2=&amp;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<CharSequence, CharSequence> 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&amp;key2=&amp;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<CharSequence, CharSequence> 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);
}
}

View File

@@ -0,0 +1,7 @@
/**
* URL相关工具
*
* @author looly
* @since 5.3.1
*/
package cn.hutool.core.net.url;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<br>
* 将需要转换的内容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码形式之外的内容用十六进制表示法转换出来并在之前加上%开头。<br>
* 此方法用于URL自动编码类似于浏览器中键入地址自动编码对于像类似于“/”的字符不再编码
*
* @param url 被编码内容
* @param url 被编码内容
* @param charset 编码
* @return 编码后的字符
* @since 4.4.1
@@ -336,7 +380,7 @@ public class URLUtil {
* 将需要转换的内容ASCII码形式之外的内容用十六进制表示法转换出来并在之前加上%开头。<br>
* 此方法用于POST请求中的请求体自动编码转义大部分特殊字符
*
* @param url 被编码内容
* @param url 被编码内容
* @param charset 编码
* @return 编码后的字符
* @since 4.4.1
@@ -356,10 +400,10 @@ public class URLUtil {
* 将需要转换的内容ASCII码形式之外的内容用十六进制表示法转换出来并在之前加上%开头。<br>
* 此方法用于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码形式之外的内容用十六进制表示法转换出来并在之前加上%开头。<br>
* 此方法用于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字符<br>
* 将%开头的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<br>
* 解码application/x-www-form-urlencoded字符<br>
* 将%开头的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. 多个/替换为一个
* </pre>
*
* @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参数形式<br>
* paramMap中如果key为空null和""会被忽略如果value为null会被做为空白符""<br>
* 会自动url编码键和值
*
* <pre>
* key1=v1&amp;key2=&amp;key3=v3
* </pre>
*
* @param paramMap 表单数据
* @param charset 编码编码为null表示不编码
* @return url参数
*/
public static String buildQuery(Map<String, ?> paramMap, Charset charset) {
return UrlQuery.of(paramMap).build(charset);
}
}

View File

@@ -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<String, Object> map = BeanUtil.beanToMap(person);
Console.log(map);
Assert.assertEquals("sub名字", map.get("aliasSubName"));
}

View File

@@ -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());
}
}