Merge remote-tracking branch 'origin/v5-dev' into v5-dev

This commit is contained in:
achao 2021-11-06 19:56:48 +08:00
commit 42a74147a6
44 changed files with 972 additions and 215 deletions

View File

@ -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 】 增加CoordinateUtilpr#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增加sm3WithSaltpr#454@Gitee
* 【http 】 增加HttpInterceptorissue#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
-------------------------------------------------------------------------------------------------------------

View File

@ -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

View File

@ -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;
}
}
/**
* 自定义函数判断集合是否包含某类值
*

View File

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

View File

@ -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();
}
// 其它情况

View File

@ -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));
}
/**
* 通过生日计算星座
*

View File

@ -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异常
*/

View File

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

View File

@ -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>
* 此正则同时支持FTPFile等协议的URL
*/
String URL_HTTP = "(https://|http://)?([\\w-]+\\.)+[\\w-]+(:\\d+)*(/[\\w- ./?%&=]*)?";
String URL_HTTP = "(https?|ftp|file)://[\\w-+&@#/%?=~_|!:,.;]*[\\w-+&@#/%=~_|]";
/**
* 中文字英文字母数字和下划线
*/

View File

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

View File

@ -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));
}
}
/**
* 设置子节点设置后会覆盖所有原有子节点
*

View File

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

View File

@ -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>
* seehttps://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 / "-" / "." / "_" / "~"
*

View File

@ -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>

View File

@ -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

View File

@ -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)) {

View File

@ -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&amp;key2=&amp;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&amp;key2=&amp;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;
}
}

View File

@ -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 切分后的集合

View File

@ -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) {

View File

@ -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 +
'}';
}
}
}

View File

@ -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表示不解码

View File

@ -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异常

View File

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

View File

@ -298,6 +298,12 @@ public class FileUtilTest {
}
}
@Test
@Ignore
public void loopFilesTest2() {
FileUtil.loopFiles("").forEach(Console::log);
}
@Test
@Ignore
public void loopFilesWithDepthTest() {

View File

@ -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 -> {

View File

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

View File

@ -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 {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 日期标准日期时间字符串
*/

View File

@ -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>
*

View File

@ -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) {
// 如果密码非空白则使用认证模式

View File

@ -11,7 +11,7 @@ port = 465
# 发件人(必须正确,否则发送失败)
from = 小磊<hutool@yeah.net>
# 用户名注意如果使用foxmail邮箱此处user为qq号
user = hutool
user = hutool@yeah.net
# 密码
pass = q1w2e3
# 使用 STARTTLS安全连接

View File

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

View File

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

View File

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

View File

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

View File

@ -11,3 +11,4 @@ homeAddress:
city: "City Y"
state: "State Y"
zip: 345657
123: 345