mirror of
https://gitee.com/dromara/hutool.git
synced 2025-04-05 17:37:59 +08:00
Merge remote-tracking branch 'origin/v5-dev' into v5-dev
This commit is contained in:
commit
42a74147a6
12
CHANGELOG.md
12
CHANGELOG.md
@ -3,7 +3,7 @@
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# 5.7.16 (2021-10-30)
|
||||
# 5.7.16 (2021-11-04)
|
||||
|
||||
### 🐣新特性
|
||||
* 【core 】 增加DateTime.toLocalDateTime
|
||||
@ -17,6 +17,15 @@
|
||||
* 【core 】 StopWatch增加prettyPrint重载(issue#1910@Github)
|
||||
* 【core 】 修改RegexPool中Ipv4正则
|
||||
* 【json 】 Filter改为MutablePair,以便编辑键值对(issue#1921@Github)
|
||||
* 【core 】 Opt增加peeks方法(pr#445@Gitee)
|
||||
* 【extra 】 MailAccount中user默认值改为邮箱全称(issue#I4FYVY@Gitee)
|
||||
* 【core 】 增加CoordinateUtil(pr#446@Gitee)
|
||||
* 【core 】 DateUtil增加rangeToList重载(pr#1925@Github)
|
||||
* 【core 】 CollUtil增加safeContains方法(pr#1926@Github)
|
||||
* 【core 】 ActualTypeMapperPool增加getStrKeyMap方法(pr#447@Gitee)
|
||||
* 【core 】 TreeUtil增加walk方法(pr#1932@Gitee)
|
||||
* 【crypto 】 SmUtil增加sm3WithSalt(pr#454@Gitee)
|
||||
* 【http 】 增加HttpInterceptor(issue#I4H1ZV@Gitee)
|
||||
|
||||
### 🐞Bug修复
|
||||
* 【core 】 修复UrlBuilder.addPath歧义问题(issue#1912@Github)
|
||||
@ -25,6 +34,7 @@
|
||||
* 【poi 】 修复合并单元格为日期时,导出单元格数据为数字问题(issue#1911@Github)
|
||||
* 【core 】 修复CompilerUtil.getFileManager参数没有使用的问题(issue#I4FIO6@Gitee)
|
||||
* 【core 】 修复NetUtil.isInRange的cidr判断问题(pr#1917@Github)
|
||||
* 【core 】 修复RegexPool中对URL正则匹配问题(issue#I4GRKD@Gitee)
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
@ -62,8 +62,11 @@ public class PercentCodec implements Serializable {
|
||||
* 存放安全编码
|
||||
*/
|
||||
private final BitSet safeCharacters;
|
||||
|
||||
/**
|
||||
* 是否编码空格为+
|
||||
* 是否编码空格为+<br>
|
||||
* 如果为{@code true},则将空格编码为"+",此项只在"application/x-www-form-urlencoded"中使用<br>
|
||||
* 如果为{@code false},则空格编码为"%20",此项一般用于URL的Query部分(RFC3986规范)
|
||||
*/
|
||||
private boolean encodeSpaceAsPlus = false;
|
||||
|
||||
@ -130,7 +133,9 @@ public class PercentCodec implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否将空格编码为+
|
||||
* 是否将空格编码为+<br>
|
||||
* 如果为{@code true},则将空格编码为"+",此项只在"application/x-www-form-urlencoded"中使用<br>
|
||||
* 如果为{@code false},则空格编码为"%20",此项一般用于URL的Query部分(RFC3986规范)
|
||||
*
|
||||
* @param encodeSpaceAsPlus 是否将空格编码为+
|
||||
* @return this
|
||||
|
@ -414,12 +414,33 @@ public class CollUtil {
|
||||
* @param collection 集合
|
||||
* @param value 需要查找的值
|
||||
* @return 如果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true}
|
||||
* @throws ClassCastException 如果类型不一致会抛出转换异常
|
||||
* @throws NullPointerException 当指定的元素 值为 null ,或集合类不支持null 时抛出该异常
|
||||
* @see Collection#contains(Object)
|
||||
* @since 4.1.10
|
||||
*/
|
||||
public static boolean contains(Collection<?> collection, Object value) {
|
||||
return isNotEmpty(collection) && collection.contains(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定集合是否包含指定值,如果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true}
|
||||
*
|
||||
* @param collection 集合
|
||||
* @param value 需要查找的值
|
||||
* @return 果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true}
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public static boolean safeContains(Collection<?> collection, Object value) {
|
||||
|
||||
try {
|
||||
return contains(collection, value);
|
||||
} catch (ClassCastException | NullPointerException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 自定义函数判断集合是否包含某类值
|
||||
*
|
||||
|
@ -1,5 +1,6 @@
|
||||
package cn.hutool.core.convert;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
@ -59,45 +60,67 @@ public class NumberChineseFormatter {
|
||||
* @return 中文
|
||||
*/
|
||||
public static String format(double amount, boolean isUseTraditional, boolean isMoneyMode) {
|
||||
if (amount > 99_9999_9999_9999.99 || amount < -99999999999999.99) {
|
||||
throw new IllegalArgumentException("Number support only: (-99999999999999.99 ~ 99999999999999.99)!");
|
||||
if(0 == amount){
|
||||
return "零";
|
||||
}
|
||||
Assert.checkBetween(amount, -99_9999_9999_9999.99, 99_9999_9999_9999.99,
|
||||
"Number support only: (-99999999999999.99 ~ 99999999999999.99)!");
|
||||
|
||||
final StringBuilder chineseStr = new StringBuilder();
|
||||
|
||||
// 负数
|
||||
boolean negative = false;
|
||||
if (amount < 0) {
|
||||
negative = true;
|
||||
chineseStr.append("负");
|
||||
amount = -amount;
|
||||
}
|
||||
|
||||
// 分和角
|
||||
long temp = Math.round(amount * 100);
|
||||
long yuan = Math.round(amount * 100);
|
||||
final int fen = (int) (yuan % 10);
|
||||
yuan = yuan / 10;
|
||||
final int jiao = (int) (yuan % 10);
|
||||
yuan = yuan / 10;
|
||||
|
||||
final int numFen = (int) (temp % 10);
|
||||
temp = temp / 10;
|
||||
final int numJiao = (int) (temp % 10);
|
||||
temp = temp / 10;
|
||||
// 元
|
||||
if(false == isMoneyMode || 0 != yuan){
|
||||
// 金额模式下,无需“零元”
|
||||
chineseStr.append(longToChinese(yuan, isUseTraditional));
|
||||
if(isMoneyMode){
|
||||
chineseStr.append("元");
|
||||
}
|
||||
}
|
||||
|
||||
final StringBuilder chineseStr = new StringBuilder(longToChinese(temp, isUseTraditional));
|
||||
//负数
|
||||
if (negative) { // 整数部分不为 0
|
||||
chineseStr.insert(0, "负");
|
||||
if(0 == jiao && 0 == fen){
|
||||
//无小数部分的金额结尾
|
||||
if(isMoneyMode){
|
||||
chineseStr.append("整");
|
||||
}
|
||||
return chineseStr.toString();
|
||||
}
|
||||
|
||||
// 小数部分
|
||||
if (numFen != 0 || numJiao != 0) {
|
||||
if (numFen == 0) {
|
||||
chineseStr.append(isMoneyMode ? "元" : "点").append(numberToChinese(numJiao, isUseTraditional)).append(isMoneyMode ? "角" : "");
|
||||
} else { // “分”数不为 0
|
||||
if (numJiao == 0) {
|
||||
chineseStr.append(isMoneyMode ? "元零" : "点零").append(numberToChinese(numFen, isUseTraditional)).append(isMoneyMode ? "分" : "");
|
||||
} else {
|
||||
chineseStr.append(isMoneyMode ? "元" : "点").append(numberToChinese(numJiao, isUseTraditional)).append(isMoneyMode ? "角" : "").append(numberToChinese(numFen, isUseTraditional)).append(isMoneyMode ? "分" : "");
|
||||
}
|
||||
if(false == isMoneyMode){
|
||||
chineseStr.append("点");
|
||||
}
|
||||
|
||||
// 角
|
||||
if(0 == yuan && 0 == jiao){
|
||||
// 元和角都为0时,只有非金额模式下补“零”
|
||||
if(false == isMoneyMode){
|
||||
chineseStr.append("零");
|
||||
}
|
||||
}else{
|
||||
chineseStr.append(numberToChinese(jiao, isUseTraditional));
|
||||
if(isMoneyMode && 0 != jiao){
|
||||
chineseStr.append("角");
|
||||
}
|
||||
}
|
||||
|
||||
// 分
|
||||
if(0 != fen){
|
||||
chineseStr.append(numberToChinese(fen, isUseTraditional));
|
||||
if(isMoneyMode){
|
||||
chineseStr.append("分");
|
||||
}
|
||||
} else if (isMoneyMode) {
|
||||
//无小数部分的金额结尾
|
||||
chineseStr.append("元整");
|
||||
}
|
||||
|
||||
return chineseStr.toString();
|
||||
|
@ -8,6 +8,7 @@ import cn.hutool.core.util.XmlUtil;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.sql.Blob;
|
||||
import java.sql.Clob;
|
||||
import java.sql.SQLException;
|
||||
@ -31,6 +32,8 @@ public class StringConverter extends AbstractConverter<String> {
|
||||
return clobToStr((Clob) value);
|
||||
} else if (value instanceof Blob) {
|
||||
return blobToStr((Blob) value);
|
||||
} else if (value instanceof Type) {
|
||||
return ((Type) value).getTypeName();
|
||||
}
|
||||
|
||||
// 其它情况
|
||||
|
@ -1877,6 +1877,20 @@ public class DateUtil extends CalendarUtil {
|
||||
return CollUtil.newArrayList((Iterable<DateTime>) range(start, end, unit));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建日期范围生成器
|
||||
*
|
||||
* @param start 起始日期时间
|
||||
* @param end 结束日期时间
|
||||
* @param unit 步进单位
|
||||
* @param step 步进
|
||||
* @return {@link DateRange}
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public static List<DateTime> rangeToList(Date start, Date end, final DateField unit, int step) {
|
||||
return CollUtil.newArrayList((Iterable<DateTime>) new DateRange(start, end, unit, step));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过生日计算星座
|
||||
*
|
||||
|
@ -223,9 +223,11 @@ public class FileUtil extends PathUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历目录以及子目录中的所有文件
|
||||
* 递归遍历目录以及子目录中的所有文件<br>
|
||||
* 如果用户传入相对路径,则是相对classpath的路径<br>
|
||||
* 如:"test/aaa"表示"${classpath}/test/aaa"
|
||||
*
|
||||
* @param path 当前遍历文件或目录的路径
|
||||
* @param path 相对ClassPath的目录或者绝对路径目录
|
||||
* @return 文件列表
|
||||
* @since 3.2.0
|
||||
*/
|
||||
@ -245,7 +247,9 @@ public class FileUtil extends PathUtil {
|
||||
|
||||
/**
|
||||
* 获得指定目录下所有文件<br>
|
||||
* 不会扫描子目录
|
||||
* 不会扫描子目录<br>
|
||||
* 如果用户传入相对路径,则是相对classpath的路径<br>
|
||||
* 如:"test/aaa"表示"${classpath}/test/aaa"
|
||||
*
|
||||
* @param path 相对ClassPath的目录或者绝对路径目录
|
||||
* @return 文件路径列表(如果是jar中的文件,则给定类似.jar!/xxx/xxx的路径)
|
||||
@ -287,7 +291,7 @@ public class FileUtil extends PathUtil {
|
||||
/**
|
||||
* 创建File对象,相当于调用new File(),不做任何处理
|
||||
*
|
||||
* @param path 文件路径
|
||||
* @param path 文件路径,相对路径表示相对项目路径
|
||||
* @return File
|
||||
* @since 4.1.4
|
||||
*/
|
||||
@ -298,7 +302,7 @@ public class FileUtil extends PathUtil {
|
||||
/**
|
||||
* 创建File对象,自动识别相对或绝对路径,相对路径将自动从ClassPath下寻找
|
||||
*
|
||||
* @param path 文件路径
|
||||
* @param path 相对ClassPath的目录或者绝对路径目录
|
||||
* @return File
|
||||
*/
|
||||
public static File file(String path) {
|
||||
@ -579,15 +583,15 @@ public class FileUtil extends PathUtil {
|
||||
* 创建文件及其父目录,如果这个文件存在,直接返回这个文件<br>
|
||||
* 此方法不对File对象类型做判断,如果File不存在,无法判断其类型
|
||||
*
|
||||
* @param fullFilePath 文件的全路径,使用POSIX风格
|
||||
* @param path 相对ClassPath的目录或者绝对路径目录,使用POSIX风格
|
||||
* @return 文件,若路径为null,返回null
|
||||
* @throws IORuntimeException IO异常
|
||||
*/
|
||||
public static File touch(String fullFilePath) throws IORuntimeException {
|
||||
if (fullFilePath == null) {
|
||||
public static File touch(String path) throws IORuntimeException {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
return touch(file(fullFilePath));
|
||||
return touch(file(path));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2978,10 +2982,11 @@ public class FileUtil extends PathUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 写数据到文件中
|
||||
* 写数据到文件中<br>
|
||||
* 文件路径如果是相对路径,则相对ClassPath
|
||||
*
|
||||
* @param data 数据
|
||||
* @param path 目标文件
|
||||
* @param path 相对ClassPath的目录或者绝对路径目录
|
||||
* @return 目标文件
|
||||
* @throws IORuntimeException IO异常
|
||||
*/
|
||||
|
@ -310,6 +310,7 @@ public class Opt<T> {
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final Opt<T> peeks(Consumer<T>... actions) throws NullPointerException {
|
||||
// 第三个参数 (opts, opt) -> null其实并不会执行到该函数式接口所以直接返回了个null
|
||||
return Stream.of(actions).reduce(this, Opt<T>::peek, (opts, opt) -> null);
|
||||
}
|
||||
|
||||
|
@ -100,14 +100,20 @@ public interface RegexPool {
|
||||
* 生日
|
||||
*/
|
||||
String BIRTHDAY = "^(\\d{2,4})([/\\-.年]?)(\\d{1,2})([/\\-.月]?)(\\d{1,2})日?$";
|
||||
/**
|
||||
* URI<br>
|
||||
* 定义见:https://www.ietf.org/rfc/rfc3986.html#appendix-B
|
||||
*/
|
||||
String URI = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?";
|
||||
/**
|
||||
* URL
|
||||
*/
|
||||
String URL = "[a-zA-z]+://[^\\s]*";
|
||||
String URL = "[a-zA-Z]+://[\\w-+&@#/%?=~_|!:,.;]*[\\w-+&@#/%=~_|]";
|
||||
/**
|
||||
* Http URL
|
||||
* Http URL(来自:http://urlregex.com/)<br>
|
||||
* 此正则同时支持FTP、File等协议的URL
|
||||
*/
|
||||
String URL_HTTP = "(https://|http://)?([\\w-]+\\.)+[\\w-]+(:\\d+)*(/[\\w- ./?%&=]*)?";
|
||||
String URL_HTTP = "(https?|ftp|file)://[\\w-+&@#/%?=~_|!:,.;]*[\\w-+&@#/%=~_|]";
|
||||
/**
|
||||
* 中文字、英文字母、数字和下划线
|
||||
*/
|
||||
|
@ -1,5 +1,6 @@
|
||||
package cn.hutool.core.lang.reflect;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.SimpleCache;
|
||||
import cn.hutool.core.util.TypeUtil;
|
||||
|
||||
@ -29,6 +30,17 @@ public class ActualTypeMapperPool {
|
||||
return CACHE.get(type, () -> createTypeMap(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取泛型变量名(字符串)和泛型实际类型的对应关系Map
|
||||
*
|
||||
* @param type 被解析的包含泛型参数的类
|
||||
* @return 泛型对应关系Map
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public static Map<String, Type> getStrKeyMap(Type type){
|
||||
return Convert.toMap(String.class, Type.class, get(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得泛型变量对应的泛型实际类型,如果此变量没有对应的实际类型,返回null
|
||||
*
|
||||
@ -89,8 +101,13 @@ public class ActualTypeMapperPool {
|
||||
final Class<?> rawType = (Class<?>) parameterizedType.getRawType();
|
||||
final Type[] typeParameters = rawType.getTypeParameters();
|
||||
|
||||
Type value;
|
||||
for (int i = 0; i < typeParameters.length; i++) {
|
||||
typeMap.put(typeParameters[i], typeArguments[i]);
|
||||
value = typeArguments[i];
|
||||
// 跳过泛型变量对应泛型变量的情况
|
||||
if(false == value instanceof TypeVariable){
|
||||
typeMap.put(typeParameters[i], value);
|
||||
}
|
||||
}
|
||||
|
||||
type = rawType;
|
||||
|
@ -12,6 +12,7 @@ import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 通过转换器将你的实体转化为TreeNodeMap节点实体 属性都存在此处,属性有序,可支持排序
|
||||
@ -174,6 +175,20 @@ import java.util.List;
|
||||
return (List<Tree<T>>) this.get(treeNodeConfig.getChildrenKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归树并处理子树下的节点:
|
||||
*
|
||||
* @param consumer 节点处理器
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public void walk(Consumer<Tree<T>> consumer) {
|
||||
consumer.accept(this);
|
||||
final List<Tree<T>> children = getChildren();
|
||||
if(CollUtil.isNotEmpty(children)){
|
||||
children.forEach((tree)-> tree.walk(consumer));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子节点,设置后会覆盖所有原有子节点
|
||||
*
|
||||
|
@ -0,0 +1,25 @@
|
||||
package cn.hutool.core.net;
|
||||
|
||||
import cn.hutool.core.codec.PercentCodec;
|
||||
|
||||
/**
|
||||
* application/x-www-form-urlencoded,遵循W3C HTML Form content types规范,如空格须转+,+须被编码<br>
|
||||
* 规范见:https://url.spec.whatwg.org/#urlencoded-serializing
|
||||
*
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public class FormUrlencoded {
|
||||
|
||||
/**
|
||||
* query中的value<br>
|
||||
* value不能包含"{@code &}",可以包含 "="
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_VALUE = PercentCodec.of(RFC3986.QUERY_PARAM_VALUE)
|
||||
.setEncodeSpaceAsPlus(true).removeSafe('+');
|
||||
|
||||
/**
|
||||
* query中的key<br>
|
||||
* key不能包含"{@code &}" 和 "="
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_NAME = QUERY_PARAM_VALUE.removeSafe('=');
|
||||
}
|
@ -3,7 +3,8 @@ package cn.hutool.core.net;
|
||||
import cn.hutool.core.codec.PercentCodec;
|
||||
|
||||
/**
|
||||
* rfc3986 : https://www.ietf.org/rfc/rfc3986.html 编码实现
|
||||
* rfc3986 : https://www.ietf.org/rfc/rfc3986.html 编码实现<br>
|
||||
* 定义见:https://www.ietf.org/rfc/rfc3986.html#appendix-A
|
||||
*
|
||||
* @author looly
|
||||
* @since 5.7.16
|
||||
@ -21,12 +22,14 @@ public class RFC3986 {
|
||||
public static final PercentCodec SUB_DELIMS = PercentCodec.of("!$&'()*+,;=");
|
||||
|
||||
/**
|
||||
* reserved = gen-delims / sub-delims
|
||||
* reserved = gen-delims / sub-delims<br>
|
||||
* see:https://www.ietf.org/rfc/rfc3986.html#section-2.2
|
||||
*/
|
||||
public static final PercentCodec RESERVED = GEN_DELIMS.orNew(SUB_DELIMS);
|
||||
|
||||
/**
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"<br>
|
||||
* see: https://www.ietf.org/rfc/rfc3986.html#section-2.3
|
||||
*/
|
||||
public static final PercentCodec UNRESERVED = PercentCodec.of(unreservedChars());
|
||||
|
||||
@ -36,7 +39,8 @@ public class RFC3986 {
|
||||
public static final PercentCodec PCHAR = UNRESERVED.orNew(SUB_DELIMS).or(PercentCodec.of(":@"));
|
||||
|
||||
/**
|
||||
* segment = pchar
|
||||
* segment = pchar<br>
|
||||
* see: https://www.ietf.org/rfc/rfc3986.html#section-3.3
|
||||
*/
|
||||
public static final PercentCodec SEGMENT = PCHAR;
|
||||
/**
|
||||
@ -60,15 +64,17 @@ public class RFC3986 {
|
||||
public static final PercentCodec FRAGMENT = QUERY;
|
||||
|
||||
/**
|
||||
* query中的key
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_NAME = PercentCodec.of(QUERY).removeSafe('&').removeSafe('=');
|
||||
|
||||
/**
|
||||
* query中的value
|
||||
* query中的value<br>
|
||||
* value不能包含"{@code &}",可以包含 "="
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_VALUE = PercentCodec.of(QUERY).removeSafe('&');
|
||||
|
||||
/**
|
||||
* query中的key<br>
|
||||
* key不能包含"{@code &}" 和 "="
|
||||
*/
|
||||
public static final PercentCodec QUERY_PARAM_NAME = QUERY_PARAM_VALUE.removeSafe('=');
|
||||
|
||||
/**
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
*
|
||||
|
@ -41,9 +41,10 @@ public class URLDecoder implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码
|
||||
* 解码<br>
|
||||
* 规则见:https://url.spec.whatwg.org/#urlencoded-parsing
|
||||
* <pre>
|
||||
* 1. 将+和%20转换为空格 ;
|
||||
* 1. 将+和%20转换为空格(" ");
|
||||
* 2. 将"%xy"转换为文本形式,xy是两位16进制的数值;
|
||||
* 3. 跳过不符合规范的%形式,直接输出
|
||||
* </pre>
|
||||
|
@ -7,7 +7,8 @@ import cn.hutool.core.util.StrUtil;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* URL编码工具
|
||||
* URL编码工具<br>
|
||||
* TODO 在6.x中移除此工具(无法很好区分URL编码和www-form编码)
|
||||
*
|
||||
* @since 5.7.13
|
||||
* @author looly
|
||||
|
@ -127,6 +127,9 @@ public class UrlPath {
|
||||
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (String segment : segments) {
|
||||
// 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义
|
||||
// path的第一部分允许有":",其余部分不允许
|
||||
// 在此处的Path部分特指host之后的部分,即不包含第一部分
|
||||
builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT_NZ_NC.encode(segment, charset));
|
||||
}
|
||||
if (withEngTag || StrUtil.isEmpty(builder)) {
|
||||
|
@ -144,6 +144,81 @@ public class UrlQuery {
|
||||
}
|
||||
}
|
||||
|
||||
return doParse(queryStr, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得查询的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键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。<br>
|
||||
* 对于{@code null}处理规则如下:
|
||||
* <ul>
|
||||
* <li>如果key为{@code null},则这个键值对忽略</li>
|
||||
* <li>如果value为{@code null},只保留key,如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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();
|
||||
CharSequence name;
|
||||
CharSequence value;
|
||||
for (Map.Entry<CharSequence, CharSequence> entry : this.query) {
|
||||
name = entry.getKey();
|
||||
if (null != name) {
|
||||
if(sb.length() >0){
|
||||
sb.append("&");
|
||||
}
|
||||
sb.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset));
|
||||
value = entry.getValue();
|
||||
if (null != value) {
|
||||
sb.append("=").append(RFC3986.QUERY_PARAM_VALUE.encode(value, charset));
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return build(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析URL中的查询字符串<br>
|
||||
* 规则见:https://url.spec.whatwg.org/#urlencoded-parsing
|
||||
*
|
||||
* @param queryStr 查询字符串,类似于key1=v1&key2=&key3=v3
|
||||
* @param charset decode编码,null表示不做decode
|
||||
* @return this
|
||||
* @since 5.5.8
|
||||
*/
|
||||
private UrlQuery doParse(String queryStr, Charset charset) {
|
||||
final int len = queryStr.length();
|
||||
String name = null;
|
||||
int pos = 0; // 未处理字符开始位置
|
||||
@ -188,80 +263,6 @@ public class UrlQuery {
|
||||
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&key2=&key3=v3形式
|
||||
*
|
||||
* @param charset encode编码,null表示不做encode编码
|
||||
* @return URL查询字符串
|
||||
*/
|
||||
public String build(Charset charset) {
|
||||
return build(charset, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL查询字符串,即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式。<br>
|
||||
* 对于{@code null}处理规则如下:
|
||||
* <ul>
|
||||
* <li>如果key为{@code null},则这个键值对忽略</li>
|
||||
* <li>如果value为{@code null},只保留key,如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param charset encode编码,null表示不做encode编码
|
||||
* @param isEncode 是否转义键和值,转义遵循rfc3986规范
|
||||
* @return URL查询字符串
|
||||
* @since 5.7.13
|
||||
*/
|
||||
public String build(Charset charset, boolean isEncode) {
|
||||
if (MapUtil.isEmpty(this.query)) {
|
||||
return StrUtil.EMPTY;
|
||||
}
|
||||
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
CharSequence name;
|
||||
CharSequence value;
|
||||
for (Map.Entry<CharSequence, CharSequence> entry : this.query) {
|
||||
name = entry.getKey();
|
||||
if (null != name) {
|
||||
if(sb.length() >0){
|
||||
sb.append("&");
|
||||
}
|
||||
sb.append(isEncode ? RFC3986.QUERY_PARAM_NAME.encode(name, charset) : name);
|
||||
value = entry.getValue();
|
||||
if (null != value) {
|
||||
sb.append("=").append(isEncode ? RFC3986.QUERY_PARAM_VALUE.encode(value, charset) : value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return build(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象转换为字符串,用于URL的Query中
|
||||
*
|
||||
@ -302,21 +303,4 @@ public class UrlQuery {
|
||||
this.query.put(URLUtil.decode(value, charset), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 键值对的name转换为
|
||||
*
|
||||
* @param str 原字符串
|
||||
* @param charset 编码,只用于encode中
|
||||
* @param isEncode 是否转义,转义遵循rfc3986规范
|
||||
* @return 转换后的String
|
||||
* @since 5.7.13
|
||||
*/
|
||||
private static String nameToStr(CharSequence str, Charset charset, boolean isEncode) {
|
||||
String result = StrUtil.str(str);
|
||||
if (isEncode) {
|
||||
result = RFC3986.QUERY_PARAM_NAME.encode(result, charset);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -243,7 +243,7 @@ public class StrSplitter {
|
||||
*
|
||||
* @param str 被切分的字符串
|
||||
* @param separator 分隔符字符串
|
||||
* @param limit 限制分片数
|
||||
* @param limit 限制分片数,小于等于0表示无限制
|
||||
* @param isTrim 是否去除切分字符串后每个元素两边的空格
|
||||
* @param ignoreEmpty 是否忽略空串
|
||||
* @return 切分后的集合
|
||||
@ -301,7 +301,7 @@ public class StrSplitter {
|
||||
*
|
||||
* @param text 被切分的字符串
|
||||
* @param separator 分隔符字符串
|
||||
* @param limit 限制分片数
|
||||
* @param limit 限制分片数,小于等于0表示无限制
|
||||
* @param isTrim 是否去除切分字符串后每个元素两边的空格
|
||||
* @param ignoreEmpty 是否忽略空串
|
||||
* @param ignoreCase 是否忽略大小写
|
||||
@ -318,7 +318,7 @@ public class StrSplitter {
|
||||
*
|
||||
* @param str 被切分的字符串
|
||||
* @param separator 分隔符字符
|
||||
* @param limit 限制分片数
|
||||
* @param limit 限制分片数,小于等于0表示无限制
|
||||
* @param isTrim 是否去除切分字符串后每个元素两边的空格
|
||||
* @param ignoreEmpty 是否忽略空串
|
||||
* @return 切分后的集合
|
||||
|
@ -40,7 +40,7 @@ public class SplitIter extends ComputeIter<String> implements Serializable {
|
||||
*
|
||||
* @param text 文本
|
||||
* @param separatorFinder 分隔符匹配器
|
||||
* @param limit 限制数量
|
||||
* @param limit 限制数量,小于等于0表示无限制
|
||||
* @param ignoreEmpty 是否忽略""
|
||||
*/
|
||||
public SplitIter(CharSequence text, TextFinder separatorFinder, int limit, boolean ignoreEmpty) {
|
||||
|
@ -0,0 +1,312 @@
|
||||
package cn.hutool.core.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 坐标系转换相关工具类,主流坐标系包括:<br>
|
||||
* <ul>
|
||||
* <li>WGS84坐标系:即地球坐标系,中国外谷歌地图</li>
|
||||
* <li>GCJ02坐标系:即火星坐标系,高德、腾讯、阿里等使用</li>
|
||||
* <li>BD09坐标系:即百度坐标系,GCJ02坐标系经加密后的坐标系。百度、搜狗等使用</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 坐标转换相关参考: https://tool.lu/coordinate/<br>
|
||||
* 参考:https://github.com/JourWon/coordinate-transform
|
||||
*
|
||||
* @author hongzhe.qin(qin462328037at163.com), looly
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public class CoordinateUtil {
|
||||
|
||||
/**
|
||||
* 坐标转换参数:(火星坐标系与百度坐标系转换的中间量)
|
||||
*/
|
||||
public static final double X_PI = 3.14159265358979324 * 3000.0 / 180.0;
|
||||
|
||||
/**
|
||||
* 坐标转换参数:π
|
||||
*/
|
||||
public static final double PI = 3.1415926535897932384626D;
|
||||
|
||||
/**
|
||||
* 地球半径(Krasovsky 1940)
|
||||
*/
|
||||
public static final double RADIUS = 6378245.0D;
|
||||
|
||||
/**
|
||||
* 修正参数(偏率ee)
|
||||
*/
|
||||
public static final double CORRECTION_PARAM = 0.00669342162296594323D;
|
||||
|
||||
/**
|
||||
* 判断坐标是否在国外<br>
|
||||
* 火星坐标系 (GCJ-02)只对国内有效,国外无需转换
|
||||
*
|
||||
* @param lng 经度
|
||||
* @param lat 纬度
|
||||
* @return 坐标是否在国外
|
||||
*/
|
||||
public static boolean outOfChina(double lng, double lat) {
|
||||
return (lng < 72.004 || lng > 137.8347) || (lat < 0.8293 || lat > 55.8271);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------- WGS84
|
||||
/**
|
||||
* WGS84 转换为 火星坐标系 (GCJ-02)
|
||||
*
|
||||
* @param lng 经度值
|
||||
* @param lat 维度值
|
||||
* @return 火星坐标 (GCJ-02)
|
||||
*/
|
||||
public static Coordinate wgs84ToGcj02(double lng, double lat) {
|
||||
return new Coordinate(lng, lat).offset(offset(lng, lat, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* WGS84 坐标转为 百度坐标系 (BD-09) 坐标
|
||||
*
|
||||
* @param lng 经度值
|
||||
* @param lat 维度值
|
||||
* @return bd09 坐标
|
||||
*/
|
||||
public static Coordinate wgs84ToBd09(double lng, double lat) {
|
||||
final Coordinate gcj02 = wgs84ToGcj02(lng, lat);
|
||||
return gcj02ToBd09(gcj02.lng, gcj02.lat);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------- GCJ-02
|
||||
/**
|
||||
* 火星坐标系 (GCJ-02) 转换为 WGS84
|
||||
*
|
||||
* @param lng 经度坐标
|
||||
* @param lat 维度坐标
|
||||
* @return WGS84 坐标
|
||||
*/
|
||||
public static Coordinate gcj02ToWgs84(double lng, double lat) {
|
||||
return new Coordinate(lng, lat).offset(offset(lng, lat, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换
|
||||
*
|
||||
* @param lng 经度值
|
||||
* @param lat 纬度值
|
||||
* @return BD-09 坐标
|
||||
*/
|
||||
public static Coordinate gcj02ToBd09(double lng, double lat) {
|
||||
double z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * X_PI);
|
||||
double theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * X_PI);
|
||||
double bd_lng = z * Math.cos(theta) + 0.0065;
|
||||
double bd_lat = z * Math.sin(theta) + 0.006;
|
||||
return new Coordinate(bd_lng, bd_lat);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------- BD-09
|
||||
/**
|
||||
* 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换
|
||||
* 即 百度 转 谷歌、高德
|
||||
*
|
||||
* @param lng 经度值
|
||||
* @param lat 纬度值
|
||||
* @return GCJ-02 坐标
|
||||
*/
|
||||
public static Coordinate bd09ToGcj02(double lng, double lat) {
|
||||
double x = lng - 0.0065;
|
||||
double y = lat - 0.006;
|
||||
double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
|
||||
double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
|
||||
double gg_lng = z * Math.cos(theta);
|
||||
double gg_lat = z * Math.sin(theta);
|
||||
return new Coordinate(gg_lng, gg_lat);
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度坐标系 (BD-09) 与 WGS84 的转换
|
||||
*
|
||||
* @param lng 经度值
|
||||
* @param lat 纬度值
|
||||
* @return WGS84坐标
|
||||
*/
|
||||
public static Coordinate bd09toWgs84(double lng, double lat) {
|
||||
final Coordinate gcj02 = bd09ToGcj02(lng, lat);
|
||||
return gcj02ToWgs84(gcj02.lng, gcj02.lat);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------- Private methods begin
|
||||
|
||||
/**
|
||||
* 转换坐标公共核心
|
||||
*
|
||||
* @param lng 经度坐标
|
||||
* @param lat 维度坐标
|
||||
* @return 返回结果
|
||||
*/
|
||||
private static double transCore(double lng, double lat) {
|
||||
double ret = (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
|
||||
ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* WGS84 与 火星坐标系 (GCJ-02)转换的偏移算法(非精确)
|
||||
*
|
||||
* @param lng 经度值
|
||||
* @param lat 纬度值
|
||||
* @param isPlus 是否正向偏移:WGS84转GCJ-02使用正向,否则使用反向
|
||||
* @return 偏移坐标
|
||||
*/
|
||||
private static Coordinate offset(double lng, double lat, boolean isPlus) {
|
||||
double dlng = transLng(lng - 105.0, lat - 35.0);
|
||||
double dlat = transLat(lng - 105.0, lat - 35.0);
|
||||
|
||||
double magic = Math.sin(lat / 180.0 * PI);
|
||||
magic = 1 - CORRECTION_PARAM * magic * magic;
|
||||
double sqrtMagic = Math.sqrt(magic);
|
||||
|
||||
dlng = (dlng * 180.0) / (RADIUS / sqrtMagic * Math.cos(lat / 180.0 * PI) * PI);
|
||||
dlat = (dlat * 180.0) / ((RADIUS * (1 - CORRECTION_PARAM)) / (magic * sqrtMagic) * PI);
|
||||
|
||||
if(false == isPlus){
|
||||
dlng = - dlng;
|
||||
dlat = - dlat;
|
||||
}
|
||||
|
||||
return new Coordinate(dlng, dlat);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算经度坐标
|
||||
*
|
||||
* @param lng 经度坐标
|
||||
* @param lat 维度坐标
|
||||
* @return ret 计算完成后的
|
||||
*/
|
||||
private static double transLng(double lng, double lat) {
|
||||
double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
|
||||
ret += transCore(lng, lat);
|
||||
ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算纬度坐标
|
||||
*
|
||||
* @param lng 经度
|
||||
* @param lat 维度
|
||||
* @return ret 计算完成后的
|
||||
*/
|
||||
private static double transLat(double lng, double lat) {
|
||||
double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
|
||||
ret += transCore(lng, lat);
|
||||
ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0;
|
||||
return ret;
|
||||
}
|
||||
//----------------------------------------------------------------------------------- Private methods end
|
||||
|
||||
/**
|
||||
* 坐标经纬度
|
||||
*
|
||||
* @author looly
|
||||
*/
|
||||
public static class Coordinate implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
private double lng;
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
private double lat;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param lng 经度
|
||||
* @param lat 纬度
|
||||
*/
|
||||
public Coordinate(double lng, double lat) {
|
||||
this.lng = lng;
|
||||
this.lat = lat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取经度
|
||||
*
|
||||
* @return 经度
|
||||
*/
|
||||
public double getLng() {
|
||||
return lng;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置经度
|
||||
*
|
||||
* @param lng 经度
|
||||
* @return this
|
||||
*/
|
||||
public Coordinate setLng(double lng) {
|
||||
this.lng = lng;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取纬度
|
||||
*
|
||||
* @return 纬度
|
||||
*/
|
||||
public double getLat() {
|
||||
return lat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纬度
|
||||
*
|
||||
* @param lat 纬度
|
||||
* @return this
|
||||
*/
|
||||
public Coordinate setLat(double lat) {
|
||||
this.lat = lat;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前坐标偏移指定坐标
|
||||
*
|
||||
* @param offset 偏移量
|
||||
* @return this
|
||||
*/
|
||||
public Coordinate offset(Coordinate offset){
|
||||
this.lng += offset.lng;
|
||||
this.lat += offset.lng;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Coordinate that = (Coordinate) o;
|
||||
return Double.compare(that.lng, lng) == 0 && Double.compare(that.lat, lat) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(lng, lat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Coordinate{" +
|
||||
"lng=" + lng +
|
||||
", lat=" + lat +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@ -319,7 +319,8 @@ public class URLUtil extends URLEncodeUtil {
|
||||
|
||||
/**
|
||||
* 解码application/x-www-form-urlencoded字符<br>
|
||||
* 将%开头的16进制表示的内容解码。
|
||||
* 将%开头的16进制表示的内容解码。<br>
|
||||
* 规则见:https://url.spec.whatwg.org/#urlencoded-parsing
|
||||
*
|
||||
* @param content 被解码内容
|
||||
* @param charset 编码,null表示不解码
|
||||
|
@ -268,7 +268,7 @@ public class ZipUtil {
|
||||
/**
|
||||
* 对文件或文件目录进行压缩
|
||||
*
|
||||
* @param zipOutputStream 生成的Zip到的目标流,不关闭此流
|
||||
* @param zipOutputStream 生成的Zip到的目标流,自动关闭此流
|
||||
* @param withSrcDir 是否包含被打包目录,只针对压缩目录有效。若为false,则只压缩目录下的文件或目录,为true则将本目录也压缩
|
||||
* @param filter 文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩)
|
||||
* @param srcFiles 要压缩的源文件或目录。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径
|
||||
@ -412,7 +412,7 @@ public class ZipUtil {
|
||||
/**
|
||||
* 将文件流压缩到目标流中
|
||||
*
|
||||
* @param zipOutputStream 目标流,压缩完成不关闭
|
||||
* @param zipOutputStream 目标流,压缩完成自动关闭
|
||||
* @param paths 流数据在压缩文件中的路径或文件名
|
||||
* @param ins 要压缩的源,添加完成后自动关闭流
|
||||
* @throws IORuntimeException IO异常
|
||||
|
@ -278,4 +278,38 @@ public class NumberChineseFormatterTest {
|
||||
// 非法字符
|
||||
NumberChineseFormatter.chineseToNumber("一百你三");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleMoneyTest(){
|
||||
String format = NumberChineseFormatter.format(0.01, false, true);
|
||||
Assert.assertEquals("一分", format);
|
||||
format = NumberChineseFormatter.format(0.10, false, true);
|
||||
Assert.assertEquals("一角", format);
|
||||
format = NumberChineseFormatter.format(0.12, false, true);
|
||||
Assert.assertEquals("一角二分", format);
|
||||
|
||||
format = NumberChineseFormatter.format(1.00, false, true);
|
||||
Assert.assertEquals("一元整", format);
|
||||
format = NumberChineseFormatter.format(1.10, false, true);
|
||||
Assert.assertEquals("一元一角", format);
|
||||
format = NumberChineseFormatter.format(1.02, false, true);
|
||||
Assert.assertEquals("一元零二分", format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleNumberTest(){
|
||||
String format = NumberChineseFormatter.format(0.01, false, false);
|
||||
Assert.assertEquals("零点零一", format);
|
||||
format = NumberChineseFormatter.format(0.10, false, false);
|
||||
Assert.assertEquals("零点一", format);
|
||||
format = NumberChineseFormatter.format(0.12, false, false);
|
||||
Assert.assertEquals("零点一二", format);
|
||||
|
||||
format = NumberChineseFormatter.format(1.00, false, false);
|
||||
Assert.assertEquals("一", format);
|
||||
format = NumberChineseFormatter.format(1.10, false, false);
|
||||
Assert.assertEquals("一点一", format);
|
||||
format = NumberChineseFormatter.format(1.02, false, false);
|
||||
Assert.assertEquals("一点零二", format);
|
||||
}
|
||||
}
|
||||
|
@ -298,6 +298,12 @@ public class FileUtilTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void loopFilesTest2() {
|
||||
FileUtil.loopFiles("").forEach(Console::log);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void loopFilesWithDepthTest() {
|
||||
|
@ -11,8 +11,6 @@ import org.junit.Test;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* {@link Opt}的单元测试
|
||||
@ -72,7 +70,7 @@ public class OptTest {
|
||||
User user = new User();
|
||||
// 相当于上面peek的动态参数调用,更加灵活,你可以像操作数组一样去动态设置中间的步骤,也可以使用这种方式去编写你的代码
|
||||
// 可以一行搞定
|
||||
Opt.ofNullable("hutool").peeks(user::setUsername, user::setNickname, System.out::println);
|
||||
Opt.ofNullable("hutool").peeks(user::setUsername, user::setNickname);
|
||||
// 也可以在适当的地方换行使得代码的可读性提高
|
||||
Opt.of(user).peeks(
|
||||
u -> Assert.assertEquals("hutool", u.getNickname()),
|
||||
@ -83,15 +81,11 @@ public class OptTest {
|
||||
|
||||
// 注意,传入的lambda中,对包裹内的元素执行赋值操作并不会影响到原来的元素,这是java语言的特性。。。
|
||||
// 这也是为什么我们需要getter和setter而不直接给bean中的属性赋值中的其中一个原因
|
||||
String name = Opt.ofNullable("hutool").peeks(username -> username = "123", username -> username = "456", n -> Assert.assertEquals("hutool", n)).get();
|
||||
String name = Opt.ofNullable("hutool").peeks(
|
||||
username -> username = "123", username -> username = "456",
|
||||
n -> Assert.assertEquals("hutool", n)).get();
|
||||
Assert.assertEquals("hutool", name);
|
||||
|
||||
// 在控制台打印n次hutool
|
||||
int n = 10;
|
||||
@SuppressWarnings("unchecked")
|
||||
Consumer<String>[] actions = Stream.<Consumer<String>>generate(() -> System.out::println).limit(n).toArray(Consumer[]::new);
|
||||
Opt.ofNullable("hutool").peeks(actions);
|
||||
|
||||
// 当然,以下情况不会抛出NPE,但也没什么意义
|
||||
Opt.ofNullable("hutool").peeks().peeks().peeks();
|
||||
Opt.ofNullable(null).peeks(i -> {
|
||||
|
@ -227,4 +227,13 @@ public class ValidatorTest {
|
||||
Validator.validateIpv4("255.255.255.255", "Error ip");
|
||||
Validator.validateIpv4("127.0.0.0", "Error ip");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isUrlTest(){
|
||||
String content = "https://detail.tmall.com/item.htm?" +
|
||||
"id=639428931841&ali_refid=a3_430582_1006:1152464078:N:Sk5vwkMVsn5O6DcnvicELrFucL21A32m:0af8611e23c1d07697e";
|
||||
|
||||
Assert.assertTrue(Validator.isMatchRegex(Validator.URL, content));
|
||||
Assert.assertTrue(Validator.isMatchRegex(Validator.URL_HTTP, content));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,60 @@
|
||||
package cn.hutool.core.lang.reflect;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 见:https://gitee.com/dromara/hutool/pulls/447/files
|
||||
*
|
||||
* TODO 同时继承泛型和实现泛型接口需要解析,此处为F
|
||||
*/
|
||||
public class ActualTypeMapperPoolTest {
|
||||
|
||||
@Test
|
||||
public void getTypeArgumentTest(){
|
||||
final Map<Type, Type> typeTypeMap = ActualTypeMapperPool.get(FinalClass.class);
|
||||
typeTypeMap.forEach((key, value)->{
|
||||
if("A".equals(key.getTypeName())){
|
||||
Assert.assertEquals(Character.class, value);
|
||||
} else if("B".equals(key.getTypeName())){
|
||||
Assert.assertEquals(Boolean.class, value);
|
||||
} else if("C".equals(key.getTypeName())){
|
||||
Assert.assertEquals(String.class, value);
|
||||
} else if("D".equals(key.getTypeName())){
|
||||
Assert.assertEquals(Double.class, value);
|
||||
} else if("E".equals(key.getTypeName())){
|
||||
Assert.assertEquals(Integer.class, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTypeArgumentStrKeyTest(){
|
||||
final Map<String, Type> typeTypeMap = ActualTypeMapperPool.getStrKeyMap(FinalClass.class);
|
||||
typeTypeMap.forEach((key, value)->{
|
||||
if("A".equals(key)){
|
||||
Assert.assertEquals(Character.class, value);
|
||||
} else if("B".equals(key)){
|
||||
Assert.assertEquals(Boolean.class, value);
|
||||
} else if("C".equals(key)){
|
||||
Assert.assertEquals(String.class, value);
|
||||
} else if("D".equals(key)){
|
||||
Assert.assertEquals(Double.class, value);
|
||||
} else if("E".equals(key)){
|
||||
Assert.assertEquals(Integer.class, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface BaseInterface<A, B, C> {}
|
||||
public interface FirstInterface<A, B, D, E> extends BaseInterface<A, B, String> {}
|
||||
public interface SecondInterface<A, B, F> extends BaseInterface<A, B, String> {}
|
||||
|
||||
public static class BaseClass<A, D> implements FirstInterface<A, Boolean, D, Integer> {}
|
||||
public static class FirstClass extends BaseClass<Character, Double> implements SecondInterface<Character, Boolean, FirstClass> {}
|
||||
public static class SecondClass extends FirstClass {}
|
||||
public static class FinalClass extends SecondClass {}
|
||||
}
|
@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -66,4 +67,13 @@ public class TreeTest {
|
||||
|
||||
Assert.assertEquals(treeNodes.size(), 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void walkTest(){
|
||||
List<String> ids = new ArrayList<>();
|
||||
final Tree<String> tree = TreeUtil.buildSingle(nodeList, "0");
|
||||
tree.walk((tr)-> ids.add(tr.getId()));
|
||||
|
||||
Assert .assertEquals(7, ids.size());
|
||||
}
|
||||
}
|
||||
|
@ -306,4 +306,11 @@ public class UrlBuilderTest {
|
||||
|
||||
Assert.assertEquals("https://domain.cn/api/xxx/bbb", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void percent2BTest(){
|
||||
String url = "http://xxx.cn/a?Signature=3R013Bj9Uq4YeISzAs2iC%2BTVCL8%3D";
|
||||
final UrlBuilder of = UrlBuilder.ofHttpWithoutEncode(url);
|
||||
Assert.assertEquals(url, of.toString());
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package cn.hutool.core.net;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.net.url.UrlBuilder;
|
||||
import cn.hutool.core.net.url.UrlQuery;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@ -99,4 +100,18 @@ public class UrlQueryTest {
|
||||
query = URLUtil.buildQuery(map, StandardCharsets.UTF_8);
|
||||
Assert.assertEquals("password==&username%3D=SSM", query);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plusTest(){
|
||||
// 根据RFC3986,在URL中,+是安全字符,即此符号不转义
|
||||
final String a = UrlQuery.of(MapUtil.of("a+b", "1+2")).build(CharsetUtil.CHARSET_UTF_8);
|
||||
Assert.assertEquals("a+b=1+2", a);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spaceTest(){
|
||||
// 根据RFC3986,在URL中,空格编码为"%20"
|
||||
final String a = UrlQuery.of(MapUtil.of("a ", " ")).build(CharsetUtil.CHARSET_UTF_8);
|
||||
Assert.assertEquals("a%20=%20", a);
|
||||
}
|
||||
}
|
||||
|
@ -122,4 +122,17 @@ public class SplitIterTest {
|
||||
final List<String> strings = splitIter.toList(false);
|
||||
Assert.assertEquals(3, strings.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitToSingleTest(){
|
||||
String text = "";
|
||||
SplitIter splitIter = new SplitIter(text,
|
||||
new CharFinder(':'),
|
||||
3,
|
||||
false
|
||||
);
|
||||
|
||||
final List<String> strings = splitIter.toList(false);
|
||||
Assert.assertEquals(1, strings.size());
|
||||
}
|
||||
}
|
||||
|
@ -54,4 +54,12 @@ public class StrSpliterTest {
|
||||
Assert.assertEquals(Long.valueOf(1L), split.get(0));
|
||||
Assert.assertEquals(Long.valueOf(2L), split.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitEmptyTest(){
|
||||
String str = "";
|
||||
final String[] split = str.split(",");
|
||||
final String[] strings = StrSplitter.splitToArray(str, ",", -1, false, false);
|
||||
Assert.assertArrayEquals(split, strings);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
package cn.hutool.core.util;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 坐标转换工具类单元测试<br>
|
||||
* 测试参考:https://github.com/wandergis/coordtransform
|
||||
*
|
||||
* @author hongzhe.qin, looly
|
||||
*/
|
||||
public class CoordinateUtilTest {
|
||||
|
||||
@Test
|
||||
public void gcj02ToBd09Test() {
|
||||
final CoordinateUtil.Coordinate gcj02 = CoordinateUtil.gcj02ToBd09(116.404, 39.915);
|
||||
Assert.assertEquals(116.41036949371029D, gcj02.getLng(), 15);
|
||||
Assert.assertEquals(39.92133699351021D, gcj02.getLat(), 15);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bd09toGcj02Test(){
|
||||
final CoordinateUtil.Coordinate gcj02 = CoordinateUtil.bd09ToGcj02(116.404, 39.915);
|
||||
Assert.assertEquals(116.39762729119315D, gcj02.getLng(), 15);
|
||||
Assert.assertEquals(39.90865673957631D, gcj02.getLat(), 15);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gcj02ToWgs84(){
|
||||
final CoordinateUtil.Coordinate gcj02 = CoordinateUtil.wgs84ToGcj02(116.404, 39.915);
|
||||
Assert.assertEquals(116.39775550083061D, gcj02.getLng(), 15);
|
||||
Assert.assertEquals(39.91359571849836D, gcj02.getLat(), 15);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wgs84ToGcj02Test(){
|
||||
final CoordinateUtil.Coordinate gcj02 = CoordinateUtil.wgs84ToGcj02(116.404, 39.915);
|
||||
Assert.assertEquals(116.41024449916938D, gcj02.getLng(), 15);
|
||||
Assert.assertEquals(39.91640428150164D, gcj02.getLat(), 15);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wgs84toBd09(){
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -11,6 +11,7 @@ import org.junit.Test;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
@ -169,15 +170,20 @@ public class ZipUtilTest {
|
||||
String file3 = "d:/test/asn1.key";
|
||||
|
||||
String zip = "d:/test/test2.zip";
|
||||
try (OutputStream out = new FileOutputStream(zip)){
|
||||
//实际应用中, out 为 HttpServletResponse.getOutputStream
|
||||
ZipUtil.zip(out, Charset.defaultCharset(), false, null,
|
||||
new File(file1),
|
||||
new File(file2),
|
||||
new File(file3)
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
//实际应用中, out 为 HttpServletResponse.getOutputStream
|
||||
ZipUtil.zip(FileUtil.getOutputStream(zip), Charset.defaultCharset(), false, null,
|
||||
new File(file1),
|
||||
new File(file2),
|
||||
new File(file3)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void zipToStreamTest(){
|
||||
String zip = "d:/test/testToStream.zip";
|
||||
OutputStream out = FileUtil.getOutputStream(zip);
|
||||
ZipUtil.zip(out, new String[]{"sm1_alias.txt"},
|
||||
new InputStream[]{FileUtil.getInputStream("d:/test/sm4_1.txt")});
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ import java.util.TimeZone;
|
||||
* 注意:
|
||||
*
|
||||
* <pre>
|
||||
* 当isMatchSecond为<code>true</code>时才会匹配秒部分
|
||||
* 当isMatchSecond为{@code true}时才会匹配秒部分
|
||||
* 默认都是关闭的
|
||||
* </pre>
|
||||
*
|
||||
@ -124,7 +124,7 @@ public class CronPattern {
|
||||
*
|
||||
* @param millis 时间毫秒数
|
||||
* @param isMatchSecond 是否匹配秒
|
||||
* @return 如果匹配返回 <code>true</code>, 否则返回 <code>false</code>
|
||||
* @return 如果匹配返回 {@code true}, 否则返回 {@code false}
|
||||
*/
|
||||
public boolean match(long millis, boolean isMatchSecond) {
|
||||
return match(TimeZone.getDefault(), millis, isMatchSecond);
|
||||
@ -136,7 +136,7 @@ public class CronPattern {
|
||||
* @param timezone 时区 {@link TimeZone}
|
||||
* @param millis 时间毫秒数
|
||||
* @param isMatchSecond 是否匹配秒
|
||||
* @return 如果匹配返回 <code>true</code>, 否则返回 <code>false</code>
|
||||
* @return 如果匹配返回 {@code true}, 否则返回 {@code false}
|
||||
*/
|
||||
public boolean match(TimeZone timezone, long millis, boolean isMatchSecond) {
|
||||
final GregorianCalendar calendar = new GregorianCalendar(timezone);
|
||||
@ -149,7 +149,7 @@ public class CronPattern {
|
||||
*
|
||||
* @param calendar 时间
|
||||
* @param isMatchSecond 是否匹配秒
|
||||
* @return 如果匹配返回 <code>true</code>, 否则返回 <code>false</code>
|
||||
* @return 如果匹配返回 {@code true}, 否则返回 {@code false}
|
||||
*/
|
||||
public boolean match(GregorianCalendar calendar, boolean isMatchSecond) {
|
||||
final int second = calendar.get(Calendar.SECOND);
|
||||
|
@ -1,13 +1,14 @@
|
||||
package cn.hutool.cron.pattern;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.cron.CronException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 定时任务单元测试类
|
||||
*
|
||||
*
|
||||
* @author Looly
|
||||
*
|
||||
*/
|
||||
@ -18,10 +19,11 @@ public class CronPatternTest {
|
||||
CronPattern pattern;
|
||||
// 任何时间匹配
|
||||
pattern = new CronPattern("* * * * * *");
|
||||
ThreadUtil.sleep(600);
|
||||
Assert.assertTrue(pattern.match(DateUtil.current(), true));
|
||||
Assert.assertTrue(pattern.match(DateUtil.current(), false));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void matchAllTest2() {
|
||||
// 在5位表达式中,秒部分并不是任意匹配,而是一个固定值
|
||||
@ -88,14 +90,14 @@ public class CronPatternTest {
|
||||
assertMatch(pattern, "2017-02-09 00:00:39");
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Test
|
||||
public void CronPatternTest2() {
|
||||
CronPattern pattern = new CronPattern("0/30 * * * *");
|
||||
Assert.assertTrue(pattern.match(DateUtil.parse("2018-10-09 12:00:00").getTime(), false));
|
||||
Assert.assertTrue(pattern.match(DateUtil.parse("2018-10-09 12:30:00").getTime(), false));
|
||||
|
||||
|
||||
pattern = new CronPattern("32 * * * *");
|
||||
Assert.assertTrue(pattern.match(DateUtil.parse("2018-10-09 12:32:00").getTime(), false));
|
||||
}
|
||||
@ -144,12 +146,12 @@ public class CronPatternTest {
|
||||
@Test(expected = CronException.class)
|
||||
public void rangeYearTest() {
|
||||
// year的范围是1970~2099年,超出报错
|
||||
CronPattern pattern = new CronPattern("0/1 * * * 1/1 ? 2020-2120");
|
||||
new CronPattern("0/1 * * * 1/1 ? 2020-2120");
|
||||
}
|
||||
|
||||
/**
|
||||
* 表达式是否匹配日期
|
||||
*
|
||||
*
|
||||
* @param pattern 表达式
|
||||
* @param date 日期,标准日期时间字符串
|
||||
*/
|
||||
|
@ -54,7 +54,7 @@ public class SmUtil {
|
||||
public static final ECDomainParameters SM2_DOMAIN_PARAMS = BCUtil.toDomainParams(GMNamedCurves.getByName(SM2_CURVE_NAME));
|
||||
/**
|
||||
* SM2国密算法公钥参数的Oid标识
|
||||
*/
|
||||
*/
|
||||
public static final ASN1ObjectIdentifier ID_SM2_PUBLIC_KEY_PARAM = new ASN1ObjectIdentifier("1.2.156.10197.1.301");
|
||||
|
||||
/**
|
||||
@ -133,6 +133,17 @@ public class SmUtil {
|
||||
return new SM3();
|
||||
}
|
||||
|
||||
/**
|
||||
* SM3加密,可以传入盐
|
||||
*
|
||||
* @param salt 加密盐
|
||||
* @return {@link SM3}
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public static SM3 sm3WithSalt(byte[] salt) {
|
||||
return new SM3(salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* SM3加密,生成16进制SM3字符串<br>
|
||||
*
|
||||
|
@ -608,8 +608,9 @@ public class MailAccount implements Serializable {
|
||||
this.host = StrUtil.format("smtp.{}", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1));
|
||||
}
|
||||
if (StrUtil.isBlank(user)) {
|
||||
// 如果用户名为空,默认为发件人邮箱前缀
|
||||
this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));
|
||||
// 如果用户名为空,默认为发件人(issue#I4FYVY@Gitee)
|
||||
//this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));
|
||||
this.user = fromAddress;
|
||||
}
|
||||
if (null == this.auth) {
|
||||
// 如果密码非空白,则使用认证模式
|
||||
|
@ -11,7 +11,7 @@ port = 465
|
||||
# 发件人(必须正确,否则发送失败)
|
||||
from = 小磊<hutool@yeah.net>
|
||||
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
|
||||
user = hutool
|
||||
user = hutool@yeah.net
|
||||
# 密码
|
||||
pass = q1w2e3
|
||||
# 使用 STARTTLS安全连接
|
||||
|
@ -0,0 +1,44 @@
|
||||
package cn.hutool.http;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Http拦截器接口,通过实现此接口,完成请求发起前对请求的编辑工作
|
||||
*
|
||||
* @author looly
|
||||
* @since 5.7.16
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface HttpInterceptor {
|
||||
|
||||
/**
|
||||
* 处理请求
|
||||
*
|
||||
* @param request 请求
|
||||
*/
|
||||
void process(HttpRequest request);
|
||||
|
||||
/**
|
||||
* 拦截器链
|
||||
*
|
||||
* @author looly
|
||||
* @since 5.7.16
|
||||
*/
|
||||
class Chain implements cn.hutool.core.lang.Chain<HttpInterceptor, Chain> {
|
||||
private final List<HttpInterceptor> interceptors = new LinkedList<>();
|
||||
|
||||
|
||||
@Override
|
||||
public Chain addChain(HttpInterceptor element) {
|
||||
interceptors.add(element);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<HttpInterceptor> iterator() {
|
||||
return interceptors.iterator();
|
||||
}
|
||||
}
|
||||
}
|
@ -88,6 +88,11 @@ public class HttpRequest extends HttpBase<HttpRequest> {
|
||||
private UrlBuilder url;
|
||||
private URLStreamHandler urlHandler;
|
||||
private Method method = Method.GET;
|
||||
/**
|
||||
* 请求前的拦截器,用于在请求前重新编辑请求
|
||||
*/
|
||||
private final HttpInterceptor.Chain interceptors = new HttpInterceptor.Chain();
|
||||
|
||||
/**
|
||||
* 默认连接超时
|
||||
*/
|
||||
@ -269,8 +274,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
|
||||
* @since 4.1.8
|
||||
*/
|
||||
public HttpRequest setUrl(String url) {
|
||||
this.url = UrlBuilder.ofHttp(url, this.charset);
|
||||
return this;
|
||||
return setUrl(UrlBuilder.ofHttp(url, this.charset));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -919,6 +923,16 @@ public class HttpRequest extends HttpBase<HttpRequest> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置拦截器,用于在请求前重新编辑请求
|
||||
*
|
||||
* @param interceptor 拦截器实现
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public void addInterceptor(HttpInterceptor interceptor) {
|
||||
this.interceptors.addChain(interceptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行Reuqest请求
|
||||
*
|
||||
@ -949,22 +963,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
|
||||
* @return this
|
||||
*/
|
||||
public HttpResponse execute(boolean isAsync) {
|
||||
// 初始化URL
|
||||
urlWithParamIfGet();
|
||||
// 初始化 connection
|
||||
initConnection();
|
||||
// 发送请求
|
||||
send();
|
||||
|
||||
// 手动实现重定向
|
||||
HttpResponse httpResponse = sendRedirectIfPossible();
|
||||
|
||||
// 获取响应
|
||||
if (null == httpResponse) {
|
||||
httpResponse = new HttpResponse(this.httpConnection, this.charset, isAsync, isIgnoreResponseBody());
|
||||
}
|
||||
|
||||
return httpResponse;
|
||||
return doExecute(isAsync, this.interceptors);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1054,6 +1053,38 @@ public class HttpRequest extends HttpBase<HttpRequest> {
|
||||
|
||||
// ---------------------------------------------------------------- Private method start
|
||||
|
||||
/**
|
||||
* 执行Reuqest请求
|
||||
*
|
||||
* @param isAsync 是否异步
|
||||
* @param interceptors 拦截器列表
|
||||
* @return this
|
||||
*/
|
||||
private HttpResponse doExecute(boolean isAsync, HttpInterceptor.Chain interceptors) {
|
||||
if (null != interceptors) {
|
||||
for (HttpInterceptor interceptor : interceptors) {
|
||||
interceptor.process(this);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化URL
|
||||
urlWithParamIfGet();
|
||||
// 初始化 connection
|
||||
initConnection();
|
||||
// 发送请求
|
||||
send();
|
||||
|
||||
// 手动实现重定向
|
||||
HttpResponse httpResponse = sendRedirectIfPossible(isAsync);
|
||||
|
||||
// 获取响应
|
||||
if (null == httpResponse) {
|
||||
httpResponse = new HttpResponse(this.httpConnection, this.charset, isAsync, isIgnoreResponseBody());
|
||||
}
|
||||
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化网络连接
|
||||
*/
|
||||
@ -1108,9 +1139,10 @@ public class HttpRequest extends HttpBase<HttpRequest> {
|
||||
/**
|
||||
* 调用转发,如果需要转发返回转发结果,否则返回{@code null}
|
||||
*
|
||||
* @param isAsync 是否异步
|
||||
* @return {@link HttpResponse},无转发返回 {@code null}
|
||||
*/
|
||||
private HttpResponse sendRedirectIfPossible() {
|
||||
private HttpResponse sendRedirectIfPossible(boolean isAsync) {
|
||||
if (this.maxRedirectCount < 1) {
|
||||
// 不重定向
|
||||
return null;
|
||||
@ -1132,7 +1164,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
|
||||
setUrl(httpConnection.header(Header.LOCATION));
|
||||
if (redirectCount < this.maxRedirectCount) {
|
||||
redirectCount++;
|
||||
return execute();
|
||||
return doExecute(isAsync, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.io.StreamProgress;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.net.RFC3986;
|
||||
import cn.hutool.core.net.url.UrlQuery;
|
||||
import cn.hutool.core.text.StrBuilder;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
@ -459,11 +460,11 @@ public class HttpUtil {
|
||||
* </pre>
|
||||
*
|
||||
* @param paramMap 表单数据
|
||||
* @param charset 编码,null表示不encode键值对
|
||||
* @param charset 编码,{@code null} 表示不encode键值对
|
||||
* @return url参数
|
||||
*/
|
||||
public static String toParams(Map<String, ?> paramMap, Charset charset) {
|
||||
return toParams(paramMap, charset, true);
|
||||
return UrlQuery.of(paramMap).build(charset);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -480,9 +481,11 @@ public class HttpUtil {
|
||||
* @param isEncode 是否转义键和值
|
||||
* @return url参数
|
||||
* @since 5.7.13
|
||||
* @deprecated 请使用 {@link #toParams(Map, Charset)}, charset为null表示不编码
|
||||
*/
|
||||
@Deprecated
|
||||
public static String toParams(Map<String, ?> paramMap, Charset charset, boolean isEncode) {
|
||||
return UrlQuery.of(paramMap).build(charset, isEncode);
|
||||
return toParams(paramMap, isEncode ? charset : null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -555,9 +558,10 @@ public class HttpUtil {
|
||||
if (null == name) {
|
||||
// 对于像&a&这类无参数值的字符串,我们将name为a的值设为""
|
||||
name = paramPart.substring(pos, i);
|
||||
builder.append(URLUtil.encodeQuery(name, charset)).append('=');
|
||||
builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=');
|
||||
} else {
|
||||
builder.append(URLUtil.encodeQuery(name, charset)).append('=').append(URLUtil.encodeQuery(paramPart.substring(pos, i), charset)).append('&');
|
||||
builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=')
|
||||
.append(RFC3986.QUERY_PARAM_VALUE.encode(paramPart.substring(pos, i), charset)).append('&');
|
||||
}
|
||||
name = null;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ public class YamlUtilTest {
|
||||
|
||||
@Test
|
||||
public void loadByPathTest() {
|
||||
final Dict result = YamlUtil.loadByPath("test.yaml", Dict.class);
|
||||
final Dict result = YamlUtil.loadByPath("test.yaml");
|
||||
|
||||
Assert.assertEquals("John", result.getStr("firstName"));
|
||||
|
||||
|
@ -11,3 +11,4 @@ homeAddress:
|
||||
city: "City Y"
|
||||
state: "State Y"
|
||||
zip: 345657
|
||||
123: 345
|
||||
|
Loading…
Reference in New Issue
Block a user