Prepare release

This commit is contained in:
Looly 2024-12-25 18:23:26 +08:00
commit ced8bf817a
63 changed files with 885 additions and 124 deletions

View File

@ -1,6 +1,26 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
# 5.8.35(2024-12-25)
### 🐣新特性
* 【poi 】 优化ExcelWriter中使用比较器writer的方法只对第一条数据进行排序pr#3807@Github
* 【extra 】 优化Ftp.download返回false抛出异常issue#3805@Github
* 【core 】 优化MAC地址正则issue#IB95X4@Gitee
* 【json 】 JSON的getByPath方法新增更为通用的指定出参类型重载pr#3814@Github
* 【core 】 DateUtil.parseUTC方法标记废弃改名为parseISO8601issue#IBB6I5@Gitee
* 【core 】 添加EnumUtil#getBy(Class, Func1, Object)方法pr#1283@Gitee
* 【db 】 添加Entity.addCondition方法issue#IBCDL2@Gitee
* 【poi 】 添加StopReadException定义sax读取时用户可手动终止issue#3820@Github
### 🐞Bug修复
* 【crypto 】 修复JWTSignerUtil.createSigner中algorithmId未转换问题issue#3806@Github
* 【core 】 修复DateUtil.rangeContains未重置问题issue#IB8OFS@Gitee
* 【cache 】 修复StampedCache类get方法并发问题issue#IBCIQG@Gitee
* 【cache 】 修复FIFOCache类使用StampedCache导致并发读的并发问题issue#IBCIQG@Gitee
* 【cache 】 废弃StampedCache可能造成Map循环调用导致死锁issue#IBDGBZ@Gitee
-------------------------------------------------------------------------------------------------------------
# 5.8.34(2024-11-25)

View File

@ -36,6 +36,9 @@
<a target="_blank" href='https://github.com/dromara/hutool'>
<img src="https://img.shields.io/github/stars/dromara/hutool.svg?style=social" alt="github star"/>
</a>
<a target="_blank" href='https://gitcode.com/dromara/hutool'>
<img src="https://gitcode.com/dromara/hutool/star/badge.svg" alt="gitcode star"/>
</a>
</p>
<br/>
@ -150,18 +153,18 @@ We provide the T-Shirt and Sweater with Hutool Logo, please visit the shop
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</dependency>
```
### 🍐Gradle
```
implementation 'cn.hutool:hutool-all:5.8.34'
implementation 'cn.hutool:hutool-all:5.8.35'
```
## 📥Download
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.34/)
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.35/)
> 🔔note:
> Hutool 5.x supports JDK8+ and is not tested on Android platforms, and cannot guarantee that all tool classes or tool methods are available.
@ -199,6 +202,7 @@ When submitting feedback, please indicate which JDK version, Hutool version, and
- [Gitee issue](https://gitee.com/dromara/hutool/issues)
- [Github issue](https://github.com/dromara/hutool/issues)
- [Gitcode issue](https://gitcode.com/dromara/hutool/issues)
### 🧬Principles of PR(pull request)

View File

@ -36,6 +36,9 @@
<a target="_blank" href='https://github.com/dromara/hutool'>
<img src="https://img.shields.io/github/stars/dromara/hutool.svg?style=social" alt="github star"/>
</a>
<a target="_blank" href='https://gitcode.com/dromara/hutool'>
<img src="https://gitcode.com/dromara/hutool/star/badge.svg" alt="gitcode star"/>
</a>
</p>
<br/>
@ -143,20 +146,20 @@ Hutool = Hu + tool是原公司项目底层代码剥离后的开源库“Hu
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</dependency>
```
### 🍐Gradle
```
implementation 'cn.hutool:hutool-all:5.8.34'
implementation 'cn.hutool:hutool-all:5.8.35'
```
### 📥下载jar
点击以下链接,下载`hutool-all-X.X.X.jar`即可:
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.34/)
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.35/)
> 🔔️注意
> Hutool 5.x支持JDK8+对Android平台没有测试不能保证所有工具类或工具方法可用。
@ -191,15 +194,16 @@ Hutool的源码分为两个分支功能如下
- [Gitee issue](https://gitee.com/dromara/hutool/issues)
- [Github issue](https://github.com/dromara/hutool/issues)
- [Gitcode issue](https://gitcode.com/dromara/hutool/issues)
### 🧬贡献代码的步骤
1. 在Gitee或者Github上fork项目到自己的repo
1. 在Gitee或者Github/Gitcode上fork项目到自己的repo
2. 把fork过去的项目也就是你的项目clone到你的本地
3. 修改代码记得一定要修改v5-dev分支
4. commit后push到自己的库v5-dev分支
5. 登录Gitee或Github在你首页可以看到一个 pull request 按钮,点击它,填写一些说明信息,然后提交即可。
5. 登录Gitee或Github/Gitcode在你首页可以看到一个 pull request 按钮,点击它,填写一些说明信息,然后提交即可。
6. 等待维护者合并
### 📐PR遵照的原则

View File

@ -3,7 +3,11 @@
echo -e "\033[32mCheckout to v5-dev\033[0m"
git checkout v5-dev
echo -e "\033[32mPush to origin v5-dev\033[0m"
echo -e "\033[32mPush to Github(origin) v5-dev\033[0m"
git push origin v5-dev
echo -e "\033[32mPush to osc v5-dev\033[0m"
echo -e "\033[32mPush to Gitee v5-dev\033[0m"
git push osc v5-dev
echo -e "\033[32mPush to Gitcode v5-dev\033[0m"
git push gitcode v5-dev

View File

@ -6,7 +6,11 @@ git checkout v5-master
echo -e "\033[32mMerge v5-dev branch\033[0m"
git merge v5-dev -m 'Prepare release'
echo -e "\033[32mPush to origin v5-master\033[0m"
echo -e "\033[32mPush to Github(origin) v5-master\033[0m"
git push origin v5-master
echo -e "\033[32mPush to osc v5-master\033[0m"
echo -e "\033[32mPush to Gitee v5-master\033[0m"
git push osc v5-master
echo -e "\033[32mPush to Gitcode v5-master\033[0m"
git push gitcode v5-master

View File

@ -3,3 +3,4 @@
git checkout v5-dev
git pull osc v5-dev
git pull origin v5-dev
git pull gitcode v5-dev

View File

@ -1 +1 @@
5.8.34
5.8.35

View File

@ -1 +1 @@
var version = '5.8.34'
var version = '5.8.35'

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-all</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-aop</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-bloomFilter</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-bom</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-cache</artifactId>

View File

@ -16,7 +16,7 @@ import java.util.LinkedHashMap;
* @param <V> 值类型
* @author Looly
*/
public class FIFOCache<K, V> extends StampedCache<K, V> {
public class FIFOCache<K, V> extends ReentrantCache<K, V> {
private static final long serialVersionUID = 1L;
/**

View File

@ -15,7 +15,7 @@ import java.util.Iterator;
* @param <K> 键类型
* @param <V> 值类型
*/
public class LFUCache<K, V> extends StampedCache<K, V> {
public class LFUCache<K, V> extends ReentrantCache<K, V> {
private static final long serialVersionUID = 1L;
/**

View File

@ -12,7 +12,9 @@ import java.util.concurrent.locks.StampedLock;
* @param <V> 值类型
* @author looly
* @since 5.7.15
* @deprecated Map使用StampedLock可能造成数据不一致甚至Map循环调用此缓存废弃
*/
@Deprecated
public abstract class StampedCache<K, V> extends AbstractCache<K, V> {
private static final long serialVersionUID = 1L;
@ -88,7 +90,12 @@ public abstract class StampedCache<K, V> extends AbstractCache<K, V> {
}
/**
* 获取值
* 获取值使用乐观锁但是此方法可能导致读取脏数据但对于缓存业务可容忍情况如下
* <pre>
* 1. 读取时无写入不冲突直接获取值
* 2. 读取时无写入但是乐观读时触发了并发异常此时获取同步锁获取新值
* 4. 读取时有写入此时获取同步锁获取新值
* </pre>
*
* @param key
* @param isUpdateLastAccess 是否更新最后修改时间
@ -97,10 +104,24 @@ public abstract class StampedCache<K, V> extends AbstractCache<K, V> {
*/
private V get(K key, boolean isUpdateLastAccess, boolean isUpdateCount) {
// 尝试读取缓存使用乐观读锁
CacheObj<K, V> co = null;
long stamp = lock.tryOptimisticRead();
CacheObj<K, V> co = getWithoutLock(key);
if (false == lock.validate(stamp)) {
// 有写线程修改了此对象悲观读
boolean isReadError = true;
if(lock.validate(stamp)){
try{
// 乐观读可能读取脏数据在缓存中可容忍分两种情况
// 1. 读取时无线程写入
// 2. 读取时有线程写入导致数据不一致此时读取未更新的缓存值
co = getWithoutLock(key);
isReadError = false;
} catch (final Exception ignore){
// ignore
}
}
if(isReadError){
// 转换为悲观读
// 原因可能为无锁读时触发并发异常或者锁被占正在写
stamp = lock.readLock();
try {
co = getWithoutLock(key);

View File

@ -17,7 +17,7 @@ import java.util.concurrent.ScheduledFuture;
* @param <K> 键类型
* @param <V> 值类型
*/
public class TimedCache<K, V> extends StampedCache<K, V> {
public class TimedCache<K, V> extends ReentrantCache<K, V> {
private static final long serialVersionUID = 1L;
/** 正在执行的定时任务 */

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-captcha</artifactId>

View File

@ -113,9 +113,7 @@ public class GifCaptcha extends AbstractCaptcha {
* @return this
*/
public GifCaptcha setRepeat(int repeat) {
if (repeat >= 0) {
this.repeat = repeat;
}
this.repeat = Math.max(repeat, 0);
return this;
}

View File

@ -0,0 +1,116 @@
package cn.hutool.captcha;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.*;
public class GifCaptchaUtilTest {
private GifCaptcha captcha;
@BeforeEach
public void setUp() {
// 初始化 GifCaptcha 类的实例
captcha = new GifCaptcha(200, 100, 4, 10); // width, height, codeCount, interfereCount
}
// 使用反射调用私有方法
private Object invokePrivateMethod(String methodName, Class<?>[] parameterTypes, Object[] parameters) throws Exception {
Method method = GifCaptcha.class.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true); // 允许访问私有方法
return method.invoke(captcha, parameters);
}
// 测试 setQuality() 方法
@Test
public void testSetQuality() throws Exception {
captcha.setQuality(20);
// 通过反射获取 quality 字段的值并进行断言
assertEquals(20, getPrivateField("quality"), "Quality 应该设置为 20");
captcha.setQuality(0); // 设置无效值应该被设置为 1
assertEquals(1, getPrivateField("quality"), "Quality 应该设置为 1如果小于 1");
}
// 测试 setRepeat() 方法
@Test
public void testSetRepeat() throws Exception {
captcha.setRepeat(5);
// 通过反射获取 repeat 字段的值并进行断言
assertEquals(5, getPrivateField("repeat"), "Repeat 应该设置为 5");
captcha.setRepeat(-1); // 设置无效值应该保持为 0
assertEquals(0, getPrivateField("repeat"), "Repeat 应该设置为 0如果设置了负值");
}
// 测试 setColorRange() 方法
@Test
public void testSetColorRange() throws Exception {
captcha.setMinColor(100).setMaxColor(200);
// 通过反射获取 minColor maxColor 字段的值并进行断言
assertEquals(100, getPrivateField("minColor"), "Min color 应该设置为 100");
assertEquals(200, getPrivateField("maxColor"), "Max color 应该设置为 200");
}
// 测试生成验证码图像的方法 createCode()
@Test
public void testCreateCode() throws Exception {
captcha.createCode();
byte[] imageBytes = captcha.getImageBytes();
// 检查生成的图片字节是否不为 null 或空
assertNotNull(imageBytes, "生成的图片字节不应该为 null");
assertTrue(imageBytes.length > 0, "生成的图片字节不应该为空");
// 可选你也可以通过解码图片字节检查它是否是有效的 GIF 格式
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(imageBytes);
// 解码图片检查它是否为有效的 GIF假设你有库可以解码 GIF
// ImageIO.read(new ByteArrayInputStream(imageBytes)); // 可以取消注释来检查它是否是有效的 GIF
}
// 测试 graphicsImage() 方法
@Test
public void testGraphicsImage() throws Exception {
char[] chars = new char[]{'A', 'B', 'C', 'D'};
Color[] colors = new Color[]{
Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW
};
// 使用反射调用 private 方法 graphicsImage
Object result = invokePrivateMethod("graphicsImage", new Class[]{char[].class, Color[].class, char[].class, int.class}, new Object[]{chars, colors, chars, 0});
assertNotNull(result, "生成的图片不应该为 null");
assertInstanceOf(BufferedImage.class, result, "返回的结果应该是 BufferedImage 类型");
}
// 测试 getRandomColor() 方法
@Test
public void testRandomColor() throws Exception {
// 使用反射调用 private 方法 getRandomColor
Object result = invokePrivateMethod("getRandomColor", new Class[]{int.class, int.class}, new Object[]{0, 255});
assertNotNull(result, "生成的颜色不应该为 null");
assertInstanceOf(Color.class, result, "返回的结果应该是 Color 类型");
Color color = (Color) result;
assertTrue(color.getRed() >= 0 && color.getRed() <= 255, "颜色的红色分量应该在 0 到 255 之间");
assertTrue(color.getGreen() >= 0 && color.getGreen() <= 255, "颜色的绿色分量应该在 0 到 255 之间");
assertTrue(color.getBlue() >= 0 && color.getBlue() <= 255, "颜色的蓝色分量应该在 0 到 255 之间");
}
// 辅助方法通过反射获取私有字段的值
private Object getPrivateField(String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = GifCaptcha.class.getDeclaredField(fieldName);
field.setAccessible(true); // 允许访问私有字段
return field.get(captcha);
}
}

View File

@ -0,0 +1,145 @@
package cn.hutool.captcha;
import cn.hutool.captcha.generator.RandomGenerator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.*;
public class ShearCaptchaTest {
private ShearCaptcha captcha;
@BeforeEach
public void setUp() {
// 初始化 ShearCaptcha 实例
captcha = new ShearCaptcha(200, 100);
}
// 测试构造函数和基本功能
@Test
public void testConstructor() {
assertNotNull(captcha, "Captcha 实例应该被成功创建");
}
// 测试生成验证码图片的功能
@Test
public void testCreateImage() {
String code = "ABCD";
Image image = captcha.createImage(code);
assertNotNull(image, "验证码图片不应该为 null");
assertInstanceOf(BufferedImage.class, image, "生成的图片应该是 BufferedImage 类型");
// 可选进一步测试图像的内容
BufferedImage bufferedImage = (BufferedImage) image;
assertEquals(200, bufferedImage.getWidth(), "图像宽度应该为 200");
assertEquals(100, bufferedImage.getHeight(), "图像高度应该为 100");
}
// 测试绘制字符串的方法
@Test
public void testDrawString() throws Exception {
String code = "ABCD";
Method drawStringMethod = ShearCaptcha.class.getDeclaredMethod("drawString", Graphics2D.class, String.class);
drawStringMethod.setAccessible(true);
Graphics2D g2d = (Graphics2D) new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();
drawStringMethod.invoke(captcha, g2d, code);
assertNotNull(g2d, "Graphics2D 对象不应该为 null");
assertTrue(g2d.getRenderingHints().containsKey(RenderingHints.KEY_ANTIALIASING), "应该启用抗锯齿");
}
// 测试 shear() 方法
@Test
public void testShear() throws Exception {
// 使用反射测试 shear 方法
Method shearMethod = ShearCaptcha.class.getDeclaredMethod("shear", Graphics.class, int.class, int.class, Color.class);
shearMethod.setAccessible(true);
Graphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();
shearMethod.invoke(captcha, g, 200, 100, Color.WHITE);
// 假设没有明显的错误输出认为测试通过
assertNotNull(g, "Graphics 对象不应该为 null");
}
// 测试 shearX() 方法
@Test
public void testShearX() throws Exception {
// 使用反射测试 shearX 方法
Method shearXMethod = ShearCaptcha.class.getDeclaredMethod("shearX", Graphics.class, int.class, int.class, Color.class);
shearXMethod.setAccessible(true);
Graphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();
shearXMethod.invoke(captcha, g, 200, 100, Color.RED);
// 假设没有明显的错误输出认为测试通过
assertNotNull(g, "Graphics 对象不应该为 null");
}
// 测试 shearY() 方法
@Test
public void testShearY() throws Exception {
// 使用反射测试 shearY 方法
Method shearYMethod = ShearCaptcha.class.getDeclaredMethod("shearY", Graphics.class, int.class, int.class, Color.class);
shearYMethod.setAccessible(true);
Graphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();
shearYMethod.invoke(captcha, g, 200, 100, Color.BLUE);
// 假设没有明显的错误输出认为测试通过
assertNotNull(g, "Graphics 对象不应该为 null");
}
// 测试 drawInterfere() 方法
@Test
public void testDrawInterfere() throws Exception {
// 使用反射测试 drawInterfere 方法
Method drawInterfereMethod = ShearCaptcha.class.getDeclaredMethod("drawInterfere", Graphics.class, int.class, int.class, int.class, int.class, int.class, Color.class);
drawInterfereMethod.setAccessible(true);
Graphics g = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB).getGraphics();
drawInterfereMethod.invoke(captcha, g, 0, 0, 200, 100, 4, Color.GREEN);
// 假设没有明显的错误输出认为测试通过
assertNotNull(g, "Graphics 对象不应该为 null");
}
// 测试验证码生成时的干扰线
@Test
public void testDrawInterfereLines() {
// 设置干扰线数量
captcha = new ShearCaptcha(200, 100, 4);
Image image = captcha.createImage("ABCD");
// 检查图像内容判断干扰线是否正确绘制
assertNotNull(image, "生成的验证码图片不应该为空");
}
// 测试验证码的尺寸
@Test
public void testCaptchaSize() {
captcha = new ShearCaptcha(300, 150);
String code = "XYZ";
Image image = captcha.createImage(code);
BufferedImage bufferedImage = (BufferedImage) image;
assertEquals(300, bufferedImage.getWidth(), "图像宽度应该为 300");
assertEquals(150, bufferedImage.getHeight(), "图像高度应该为 150");
}
// 测试生成随机验证码字符
@Test
public void testRandomGenerator() {
RandomGenerator randomGenerator = new RandomGenerator(4);
String code = randomGenerator.generate();
assertNotNull(code, "生成的验证码字符不应该为 null");
assertEquals(4, code.length(), "验证码字符长度应该为 4");
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-core</artifactId>

View File

@ -844,16 +844,37 @@ public class DateUtil extends CalendarUtil {
* @param utcString UTC时间
* @return 日期对象
* @since 4.1.14
* @deprecated 方法歧义带T的日期并不一定是UTC时间请使用 {@link #parseISO8601(String)}
*/
@Deprecated
public static DateTime parseUTC(String utcString) {
if (utcString == null) {
return parseISO8601(utcString);
}
/**
* 解析ISO8601时间格式<br>
* <ol>
* <li>yyyy-MM-dd'T'HH:mm:ss'Z'</li>
* <li>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</li>
* <li>yyyy-MM-dd'T'HH:mm:ssZ</li>
* <li>yyyy-MM-dd'T'HH:mm:ss.SSSZ</li>
* <li>yyyy-MM-dd'T'HH:mm:ss+0800</li>
* <li>yyyy-MM-dd'T'HH:mm:ss+08:00</li>
* </ol>
*
* @param iso8601String ISO8601时间
* @return 日期对象
* @since 5.8.34
*/
public static DateTime parseISO8601(String iso8601String) {
if (iso8601String == null) {
return null;
}
final int length = utcString.length();
if (StrUtil.contains(utcString, 'Z')) {
final int length = iso8601String.length();
if (StrUtil.contains(iso8601String, 'Z')) {
if (length == DatePattern.UTC_PATTERN.length() - 4) {
// 格式类似2018-09-13T05:34:31Z-4表示减去4个单引号的长度
return parse(utcString, DatePattern.UTC_FORMAT);
return parse(iso8601String, DatePattern.UTC_FORMAT);
}
final int patternLength = DatePattern.UTC_MS_PATTERN.length();
@ -861,61 +882,61 @@ public class DateUtil extends CalendarUtil {
// -4 ~ -6范围表示匹配毫秒1~3位的情况
if (length <= patternLength && length >= patternLength - 6) {
// issue#I7H34N支持最多6位毫秒
return parse(utcString, DatePattern.UTC_MS_FORMAT);
return parse(iso8601String, DatePattern.UTC_MS_FORMAT);
}
} else if (StrUtil.contains(utcString, '+')) {
} else if (StrUtil.contains(iso8601String, '+')) {
// 去除类似2019-06-01T19:45:43 +08:00加号前的空格
utcString = utcString.replace(" +", "+");
final String zoneOffset = StrUtil.subAfter(utcString, '+', true);
iso8601String = iso8601String.replace(" +", "+");
final String zoneOffset = StrUtil.subAfter(iso8601String, '+', true);
if (StrUtil.isBlank(zoneOffset)) {
throw new DateException("Invalid format: [{}]", utcString);
throw new DateException("Invalid format: [{}]", iso8601String);
}
if (false == StrUtil.contains(zoneOffset, ':')) {
// +0800转换为+08:00
final String pre = StrUtil.subBefore(utcString, '+', true);
utcString = pre + "+" + zoneOffset.substring(0, 2) + ":" + "00";
final String pre = StrUtil.subBefore(iso8601String, '+', true);
iso8601String = pre + "+" + zoneOffset.substring(0, 2) + ":" + "00";
}
if (StrUtil.contains(utcString, CharUtil.DOT)) {
if (StrUtil.contains(iso8601String, CharUtil.DOT)) {
// 带毫秒格式类似2018-09-13T05:34:31.999+08:00
utcString = normalizeMillSeconds(utcString, ".", "+");
return parse(utcString, DatePattern.UTC_MS_WITH_XXX_OFFSET_FORMAT);
iso8601String = normalizeMillSeconds(iso8601String, ".", "+");
return parse(iso8601String, DatePattern.UTC_MS_WITH_XXX_OFFSET_FORMAT);
} else {
// 格式类似2018-09-13T05:34:31+08:00
return parse(utcString, DatePattern.UTC_WITH_XXX_OFFSET_FORMAT);
return parse(iso8601String, DatePattern.UTC_WITH_XXX_OFFSET_FORMAT);
}
} else if(ReUtil.contains("-\\d{2}:?00", utcString)){
} else if(ReUtil.contains("-\\d{2}:?00", iso8601String)){
// Issue#2612类似 2022-09-14T23:59:00-08:00 或者 2022-09-14T23:59:00-0800
// 去除类似2019-06-01T19:45:43 -08:00加号前的空格
utcString = utcString.replace(" -", "-");
if(':' != utcString.charAt(utcString.length() - 3)){
utcString = utcString.substring(0, utcString.length() - 2) + ":00";
iso8601String = iso8601String.replace(" -", "-");
if(':' != iso8601String.charAt(iso8601String.length() - 3)){
iso8601String = iso8601String.substring(0, iso8601String.length() - 2) + ":00";
}
if (StrUtil.contains(utcString, CharUtil.DOT)) {
if (StrUtil.contains(iso8601String, CharUtil.DOT)) {
// 带毫秒格式类似2018-09-13T05:34:31.999-08:00
utcString = normalizeMillSeconds(utcString, ".", "-");
return new DateTime(utcString, DatePattern.UTC_MS_WITH_XXX_OFFSET_FORMAT);
iso8601String = normalizeMillSeconds(iso8601String, ".", "-");
return new DateTime(iso8601String, DatePattern.UTC_MS_WITH_XXX_OFFSET_FORMAT);
} else {
// 格式类似2018-09-13T05:34:31-08:00
return new DateTime(utcString, DatePattern.UTC_WITH_XXX_OFFSET_FORMAT);
return new DateTime(iso8601String, DatePattern.UTC_WITH_XXX_OFFSET_FORMAT);
}
} else {
if (length == DatePattern.UTC_SIMPLE_PATTERN.length() - 2) {
// 格式类似2018-09-13T05:34:31
return parse(utcString, DatePattern.UTC_SIMPLE_FORMAT);
return parse(iso8601String, DatePattern.UTC_SIMPLE_FORMAT);
} else if (length == DatePattern.UTC_SIMPLE_PATTERN.length() - 5) {
// 格式类似2018-09-13T05:34
return parse(utcString + ":00", DatePattern.UTC_SIMPLE_FORMAT);
} else if (StrUtil.contains(utcString, CharUtil.DOT)) {
return parse(iso8601String + ":00", DatePattern.UTC_SIMPLE_FORMAT);
} else if (StrUtil.contains(iso8601String, CharUtil.DOT)) {
// 可能为 2021-03-17T06:31:33.99
utcString = normalizeMillSeconds(utcString, ".", null);
return parse(utcString, DatePattern.UTC_SIMPLE_MS_FORMAT);
iso8601String = normalizeMillSeconds(iso8601String, ".", null);
return parse(iso8601String, DatePattern.UTC_SIMPLE_MS_FORMAT);
}
}
// 没有更多匹配的时间格式
throw new DateException("No format fit for date String [{}] !", utcString);
throw new DateException("No format fit for date String [{}] !", iso8601String);
}
/**
@ -1025,8 +1046,8 @@ public class DateUtil extends CalendarUtil {
// Wed Aug 01 00:00:00 CST 2012
return parseRFC2822(dateStr);
} else if (StrUtil.contains(dateStr, 'T')) {
// UTC时间
return parseUTC(dateStr);
// ISO8601时间
return parseISO8601(dateStr);
}
//标准日期格式包括单个数字的日期时间
@ -1975,8 +1996,8 @@ public class DateUtil extends CalendarUtil {
* @since 5.7.21
*/
public static List<DateTime> rangeContains(DateRange start, DateRange end) {
List<DateTime> startDateTimes = CollUtil.newArrayList((Iterable<DateTime>) start);
List<DateTime> endDateTimes = CollUtil.newArrayList((Iterable<DateTime>) end);
List<DateTime> startDateTimes = CollUtil.newArrayList((Iterable<DateTime>) start.reset());
List<DateTime> endDateTimes = CollUtil.newArrayList((Iterable<DateTime>) end.reset());
return startDateTimes.stream().filter(endDateTimes::contains).collect(Collectors.toList());
}
@ -1990,8 +2011,8 @@ public class DateUtil extends CalendarUtil {
* @since 5.7.21
*/
public static List<DateTime> rangeNotContains(DateRange start, DateRange end) {
List<DateTime> startDateTimes = CollUtil.newArrayList((Iterable<DateTime>) start);
List<DateTime> endDateTimes = CollUtil.newArrayList((Iterable<DateTime>) end);
List<DateTime> startDateTimes = CollUtil.newArrayList((Iterable<DateTime>) start.reset());
List<DateTime> endDateTimes = CollUtil.newArrayList((Iterable<DateTime>) end.reset());
return endDateTimes.stream().filter(item -> !startDateTimes.contains(item)).collect(Collectors.toList());
}

View File

@ -132,7 +132,8 @@ public interface RegexPool {
/**
* MAC地址正则
*/
String MAC_ADDRESS = "((?:[a-fA-F0-9]{1,2}[:-]){5}[a-fA-F0-9]{1,2})|0x(\\d{12}).+ETHER";
//String MAC_ADDRESS = "((?:[a-fA-F0-9]{1,2}[:-]){5}[a-fA-F0-9]{1,2})|0x(\\d{12}).+ETHER";
String MAC_ADDRESS = "((?:[a-fA-F0-9]{1,2}[:-]){5}[a-fA-F0-9]{1,2})|((?:[a-fA-F0-9]{1,4}[.]){2}[a-fA-F0-9]{1,4})|[a-fA-F0-9]{12}|0x(\\d{12}).+ETHER";
/**
* 16进制字符串
*/

View File

@ -60,11 +60,11 @@ public class EnumUtil {
* @since 5.1.6
*/
public static <E extends Enum<E>> E getEnumAt(Class<E> enumClass, int index) {
if(null == enumClass){
if (null == enumClass) {
return null;
}
final E[] enumConstants = enumClass.getEnumConstants();
if(index < 0){
if (index < 0) {
index = enumConstants.length + index;
}
@ -129,7 +129,7 @@ public class EnumUtil {
*/
@SuppressWarnings("unchecked")
public static <E extends Enum<E>> E likeValueOf(Class<E> enumClass, Object value) {
if(null == enumClass || null == value){
if (null == enumClass || null == value) {
return null;
}
if (value instanceof CharSequence) {
@ -161,7 +161,7 @@ public class EnumUtil {
* @return name列表
*/
public static List<String> getNames(Class<? extends Enum<?>> clazz) {
if(null == clazz){
if (null == clazz) {
return null;
}
final Enum<?>[] enums = clazz.getEnumConstants();
@ -183,7 +183,7 @@ public class EnumUtil {
* @return 字段值列表
*/
public static List<Object> getFieldValues(Class<? extends Enum<?>> clazz, String fieldName) {
if(null == clazz || StrUtil.isBlank(fieldName)){
if (null == clazz || StrUtil.isBlank(fieldName)) {
return null;
}
final Enum<?>[] enums = clazz.getEnumConstants();
@ -210,7 +210,7 @@ public class EnumUtil {
* @since 4.1.20
*/
public static List<String> getFieldNames(Class<? extends Enum<?>> clazz) {
if(null == clazz){
if (null == clazz) {
return null;
}
final List<String> names = new ArrayList<>();
@ -238,11 +238,62 @@ public class EnumUtil {
* @since 5.8.0
*/
public static <E extends Enum<E>> E getBy(Class<E> enumClass, Predicate<? super E> predicate) {
if(null == enumClass || null == predicate){
return getBy(enumClass, predicate, null);
}
/**
* 通过 某字段对应值 获取 枚举获取不到时为 {@code defaultEnum}
*
* @param enumClass 枚举类
* @param predicate 条件
* @param defaultEnum 获取不到时的默认枚举值
* @param <E> 枚举类型
* @return 对应枚举 获取不到时为 {@code defaultEnum}
*/
public static <E extends Enum<E>> E getBy(Class<E> enumClass, Predicate<? super E> predicate, E defaultEnum) {
if (null == enumClass || null == predicate) {
return null;
}
return Arrays.stream(enumClass.getEnumConstants())
.filter(predicate).findFirst().orElse(null);
.filter(predicate).findFirst().orElse(defaultEnum);
}
/**
* 通过 某字段对应值 获取 枚举获取不到时为 {@code null}
* <p>
* {@link LambdaUtil#getRealClass(Func1)}} 是相对耗时的
* 如果枚举值比较多,那么{@link EnumUtil#getBy(Func1, Object)} 方法
* 大部分时间都是被{@link LambdaUtil#getRealClass(Func1)}}所消耗的
* <br>
* 如果可以在编码过程中可以提供对应的枚举类 该方法与枚举的{@code Enum.values()}方法是差不多的
*
* @param enumClass 枚举类 {@code null}返回{@code null}
* @param condition 条件字段{@code null}返回{@code null}
* @param value 条件字段值
* @param <E> 枚举类型
* @param <C> 字段类型
* @return 对应枚举 获取不到时为 {@code null}
*/
public static <E extends Enum<E>, C> E getBy(Class<E> enumClass, Func1<E, C> condition, C value) {
if (null == condition) {
return null;
}
return getBy(enumClass, constant -> ObjUtil.equals(condition.callWithRuntimeException(constant), value));
}
/**
* 通过 某字段对应值 获取 枚举获取不到时为 {@code defaultEnum}
*
* @param enumClass 枚举类 {@code null}返回{@code null}
* @param condition 条件字段{@code null}返回{@code null}
* @param value 条件字段值
* @param defaultEnum 获取不到时的默认枚举值
* @param <E> 枚举类型
* @param <C> 字段类型
* @return 对应枚举 获取不到时为 {@code defaultEnum}
*/
public static <E extends Enum<E>, C> E getBy(Class<E> enumClass, Func1<E, C> condition, C value, E defaultEnum) {
return ObjectUtil.defaultIfNull(getBy(enumClass, condition, value), defaultEnum);
}
/**
@ -259,10 +310,7 @@ public class EnumUtil {
return null;
}
final Class<E> implClass = LambdaUtil.getRealClass(condition);
return Arrays.stream(implClass.getEnumConstants())
.filter(constant -> ObjUtil.equals(condition.callWithRuntimeException(constant), value))
.findAny()
.orElse(null);
return getBy(implClass, condition, value);
}
/**
@ -293,7 +341,7 @@ public class EnumUtil {
* @since 5.8.0
*/
public static <E extends Enum<E>, F, C> F getFieldBy(Func1<E, F> field, Function<E, C> condition, C value) {
if(null == field || null == condition){
if (null == field || null == condition) {
return null;
}
final Class<E> implClass = LambdaUtil.getRealClass(field);
@ -316,7 +364,7 @@ public class EnumUtil {
* @since 4.0.2
*/
public static <E extends Enum<E>> LinkedHashMap<String, E> getEnumMap(final Class<E> enumClass) {
if(null == enumClass){
if (null == enumClass) {
return null;
}
final LinkedHashMap<String, E> map = new LinkedHashMap<>();
@ -335,7 +383,7 @@ public class EnumUtil {
* @return 枚举名对应指定字段值的Map
*/
public static Map<String, Object> getNameFieldMap(Class<? extends Enum<?>> clazz, String fieldName) {
if(null == clazz || StrUtil.isBlank(fieldName)){
if (null == clazz || StrUtil.isBlank(fieldName)) {
return null;
}
final Enum<?>[] enums = clazz.getEnumConstants();
@ -359,7 +407,7 @@ public class EnumUtil {
*/
public static <E extends Enum<E>> boolean contains(final Class<E> enumClass, String val) {
final LinkedHashMap<String, E> enumMap = getEnumMap(enumClass);
if(CollUtil.isEmpty(enumMap)){
if (CollUtil.isEmpty(enumMap)) {
return false;
}
return enumMap.containsKey(val);

View File

@ -1,12 +1,13 @@
package cn.hutool.core.date;
import cn.hutool.core.date.BetweenFormatter.Level;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class DateBetweenTest {
@Test
@ -36,6 +37,14 @@ public class DateBetweenTest {
assertEquals(18, betweenYear);
}
@Test
public void betweenYearTest3() {
Date start = DateUtil.parse("20170301");
Date end = DateUtil.parse("2024-02-29 14:56:18");
long betweenYear = new DateBetween(start, end).betweenYear(false);
assertEquals(6, betweenYear);
}
@Test
public void betweenMonthTest() {
Date start = DateUtil.parse("2017-02-01 12:23:46");

View File

@ -605,9 +605,9 @@ public class DateUtilTest {
}
@Test
public void parseUTCTest() {
public void parseISO8601Test() {
String dateStr1 = "2018-09-13T05:34:31Z";
DateTime dt = DateUtil.parseUTC(dateStr1);
DateTime dt = DateUtil.parseISO8601(dateStr1);
// parse方法支持UTC格式测试
final DateTime dt2 = DateUtil.parse(dateStr1);
@ -622,12 +622,12 @@ public class DateUtilTest {
assertEquals("2018-09-13 13:34:31", dateStr);
dateStr1 = "2018-09-13T13:34:32+0800";
dt = DateUtil.parseUTC(dateStr1);
dt = DateUtil.parseISO8601(dateStr1);
dateStr = dt.toString(TimeZone.getTimeZone("GMT+8:00"));
assertEquals("2018-09-13 13:34:32", dateStr);
dateStr1 = "2018-09-13T13:34:33+08:00";
dt = DateUtil.parseUTC(dateStr1);
dt = DateUtil.parseISO8601(dateStr1);
dateStr = dt.toString(TimeZone.getTimeZone("GMT+8:00"));
assertEquals("2018-09-13 13:34:33", dateStr);
@ -644,14 +644,14 @@ public class DateUtilTest {
assertEquals("2018-09-13 13:34:35", dateStr);
dateStr1 = "2018-09-13T13:34:36.999+0800";
dt = DateUtil.parseUTC(dateStr1);
dt = DateUtil.parseISO8601(dateStr1);
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DatePattern.NORM_DATETIME_MS_PATTERN);
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
dateStr = dt.toString(simpleDateFormat);
assertEquals("2018-09-13 13:34:36.999", dateStr);
dateStr1 = "2018-09-13T13:34:37.999+08:00";
dt = DateUtil.parseUTC(dateStr1);
dt = DateUtil.parseISO8601(dateStr1);
dateStr = dt.toString(simpleDateFormat);
assertEquals("2018-09-13 13:34:37.999", dateStr);
@ -676,19 +676,19 @@ public class DateUtilTest {
}
@Test
public void parseUTCTest2() {
public void parseUTCTest() {
// issue1503@Github
// 检查不同毫秒长度都可以正常匹配
String utcTime = "2021-03-30T12:56:51.3Z";
DateTime parse = DateUtil.parseUTC(utcTime);
DateTime parse = DateUtil.parseISO8601(utcTime);
assertEquals("2021-03-30 12:56:51", parse.toString());
utcTime = "2021-03-30T12:56:51.34Z";
parse = DateUtil.parseUTC(utcTime);
parse = DateUtil.parseISO8601(utcTime);
assertEquals("2021-03-30 12:56:51", parse.toString());
utcTime = "2021-03-30T12:56:51.345Z";
parse = DateUtil.parseUTC(utcTime);
parse = DateUtil.parseISO8601(utcTime);
assertEquals("2021-03-30 12:56:51", parse.toString());
}
@ -994,7 +994,7 @@ public class DateUtilTest {
@SuppressWarnings("ConstantConditions")
@Test
public void parseISO8601Test() {
public void parseWithMilsTest() {
final String dt = "2020-06-03 12:32:12,333";
final DateTime parse = DateUtil.parse(dt);
assertEquals("2020-06-03 12:32:12", parse.toString());

View File

@ -0,0 +1,25 @@
package cn.hutool.core.date;
import cn.hutool.core.lang.Console;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
public class IssueIB8OFSTest {
@Test
void rangeTest() {
DateRange startRange = DateUtil.range(
DateUtil.parse("2017-01-01"),
DateUtil.parse("2017-01-31"), DateField.DAY_OF_YEAR);
DateRange endRange = DateUtil.range(
DateUtil.parse("2017-01-31"),
DateUtil.parse("2017-02-02"), DateField.DAY_OF_YEAR);
List<DateTime> dateTimes = DateUtil.rangeContains(startRange, endRange);
Assertions.assertEquals(1, dateTimes.size());
List<DateTime> dateNotTimes = DateUtil.rangeNotContains(startRange, endRange);
Assertions.assertEquals(2, dateNotTimes.size());
}
}

View File

@ -0,0 +1,12 @@
package cn.hutool.core.date;
import org.junit.jupiter.api.Test;
import java.text.SimpleDateFormat;
public class IssueIB9NPUTest {
@Test
void parseTest() {
DateUtil.parse("202409032400", new SimpleDateFormat("yyyyMMddHHmm"));
}
}

View File

@ -0,0 +1,25 @@
package cn.hutool.core.date;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.ZoneId;
import java.util.TimeZone;
public class IssueIBB6I5Test {
@Test
void parseISO8601Test() {
DateTime date = DateUtil.parseISO8601("2024-12-13T08:02:27Z");
TimeZone timeZone = TimeZone.getTimeZone(ZoneId.of("Asia/Shanghai"));
date.setTimeZone(timeZone);
Assertions.assertEquals("2024-12-13 16:02:27", date.toString());
}
@Test
void parseISO8601Test2() {
DateTime date = DateUtil.parseISO8601("2024-12-13T08:02:27");
TimeZone timeZone = TimeZone.getTimeZone(ZoneId.of("Asia/Shanghai"));
date.setTimeZone(timeZone);
Assertions.assertEquals("2024-12-13 08:02:27", date.toString());
}
}

View File

@ -0,0 +1,16 @@
package cn.hutool.core.util;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class Issue3809Test {
@Test
void roundStrTest() {
Assertions.assertEquals("9999999999999999.99", NumberUtil.roundStr("9999999999999999.99", 2)); //输出结果不符合方法声明返回值规则
Assertions.assertEquals("11111111111111119.00", NumberUtil.roundStr("11111111111111119.00", 2));
Assertions.assertEquals("7999999999999999.99", NumberUtil.roundStr("7999999999999999.99", 2)); //输出结果不符合方法声明返回值规则
Assertions.assertEquals("699999999991999.92", NumberUtil.roundStr("699999999991999.92", 2)); //输出结果不符合方法声明返回值规则
Assertions.assertEquals("10.92", NumberUtil.roundStr("10.92", 2));
Assertions.assertEquals("10.99", NumberUtil.roundStr("10.99", 2));
}
}

View File

@ -0,0 +1,16 @@
package cn.hutool.core.util;
import cn.hutool.core.lang.PatternPool;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class IssueIB95X4Test {
@Test
void isMacTest() {
Assertions.assertTrue(ReUtil.isMatch(PatternPool.MAC_ADDRESS, "ab1c.2d3e.f468"));
Assertions.assertTrue(ReUtil.isMatch(PatternPool.MAC_ADDRESS, "ab:1c:2d:3e:f4:68"));
Assertions.assertTrue(ReUtil.isMatch(PatternPool.MAC_ADDRESS, "ab-1c-2d-3e-f4-68"));
Assertions.assertTrue(ReUtil.isMatch(PatternPool.MAC_ADDRESS, "ab1c2d3ef468"));
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-cron</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-crypto</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-db</artifactId>

View File

@ -8,6 +8,7 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.sql.Condition;
import cn.hutool.db.sql.SqlUtil;
import java.nio.charset.Charset;
@ -266,6 +267,17 @@ public class Entity extends Dict {
}
// -------------------------------------------------------------------- Put and Set start
/**
* 添加条件
*
* @param condition 条件
* @return this
* @since 5.8.34
*/
public Entity addCondition(final Condition condition) {
return set(condition.getField(), condition);
}
@Override
public Entity set(String field, Object value) {
return (Entity) super.set(field, value);

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-dfa</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-extra</artifactId>

View File

@ -708,9 +708,11 @@ public class Ftp extends AbstractFtp {
if (null != fileNameCharset) {
fileName = new String(fileName.getBytes(fileNameCharset), StandardCharsets.ISO_8859_1);
}
boolean isSuccess;
try {
client.setFileType(FTPClient.BINARY_FILE_TYPE);
client.retrieveFile(fileName, out);
isSuccess = client.retrieveFile(fileName, out);
} catch (IOException e) {
throw new IORuntimeException(e);
} finally {
@ -718,6 +720,10 @@ public class Ftp extends AbstractFtp {
cd(pwd);
}
}
if(false == isSuccess){
throw new FtpException("retrieveFile return false");
}
}
/**

View File

@ -169,7 +169,7 @@ public class Sftp extends AbstractFtp {
*/
public void init() {
// issue#IB69U8 如果用户传入Session对象则不能使用配置初始化而是尝试重新连接
if(StrUtil.isEmpty(this.ftpConfig.getHost()) && null != this.session){
if(null != this.session){
try {
this.session.connect((int) this.ftpConfig.getConnectionTimeout());
} catch (JSchException e) {

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-http</artifactId>

View File

@ -0,0 +1,15 @@
package cn.hutool.http;
import cn.hutool.core.lang.Console;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class IssueIB7REWTest {
@Test
@Disabled
void getTest() {
System.setProperty("jdk.tls.namedCurves", "secp256r1,secp384r1,secp521r1");
final String s = HttpUtil.get("https://ebssec.boc.cn/");
Console.log(s);
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-json</artifactId>

View File

@ -97,6 +97,32 @@ public interface JSON extends Cloneable, Serializable, IJSONTypeConverter {
*/
<T> T getByPath(String expression, Class<T> resultType);
/**
* 通过表达式获取JSON中嵌套的对象<br>
* <ol>
* <li>.表达式可以获取Bean对象中的属性字段值或者Map中key对应的值</li>
* <li>[]表达式可以获取集合等对象中对应index的值</li>
* </ol>
* <p>
* 表达式栗子
*
* <pre>
* persion
* persion.name
* persons[3]
* person.friends[5].name
* </pre>
* <p>
* 获取表达式对应值后转换为对应类型的值
*
* @param expression 表达式
* @param targetType 返回值类型
* @return 对象
* @see BeanPath#get(Object)
* @since 5.8.34
*/
<T> T getByPath(String expression, TypeReference<T> targetType);
/**
* 格式化打印JSON缩进为4个空格
*

View File

@ -3,6 +3,7 @@ package cn.hutool.json;
import cn.hutool.core.bean.BeanPath;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.lang.mutable.Mutable;
import cn.hutool.core.lang.mutable.MutableObj;
@ -13,6 +14,7 @@ import cn.hutool.json.serialize.JSONWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
@ -195,7 +197,7 @@ public class JSONArray implements JSON, JSONGetter<Integer>, List<Object>, Rando
*/
public String join(String separator) throws JSONException {
return StrJoiner.of(separator)
.append(this, InternalJSONUtil::valueToString).toString();
.append(this, InternalJSONUtil::valueToString).toString();
}
@Override
@ -218,6 +220,11 @@ public class JSONArray implements JSON, JSONGetter<Integer>, List<Object>, Rando
return JSONConverter.jsonConvert(resultType, getByPath(expression), getConfig());
}
@Override
public <T> T getByPath(String expression, TypeReference<T> targetType) {
return JSONConverter.jsonConvert(targetType, getByPath(expression), getConfig());
}
@Override
public void putByPath(String expression, Object value) {
BeanPath.create(expression).set(this, value);

View File

@ -3,6 +3,7 @@ package cn.hutool.json;
import cn.hutool.core.bean.BeanPath;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.lang.mutable.MutablePair;
import cn.hutool.core.map.CaseInsensitiveMap;
import cn.hutool.core.map.MapUtil;
@ -14,6 +15,7 @@ import cn.hutool.json.serialize.JSONWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
@ -161,8 +163,8 @@ public class JSONObject extends MapWrapper<String, Object> implements JSON, JSON
@Deprecated
public JSONObject(Object source, boolean ignoreNullValue, boolean isOrder) {
this(source, JSONConfig.create()//
.setIgnoreCase((source instanceof CaseInsensitiveMap))//
.setIgnoreNullValue(ignoreNullValue)
.setIgnoreCase((source instanceof CaseInsensitiveMap))//
.setIgnoreNullValue(ignoreNullValue)
);
}
@ -320,6 +322,11 @@ public class JSONObject extends MapWrapper<String, Object> implements JSON, JSON
return JSONConverter.jsonConvert(resultType, getByPath(expression), getConfig());
}
@Override
public <T> T getByPath(String expression, TypeReference<T> targetType) {
return JSONConverter.jsonConvert(targetType, getByPath(expression), getConfig());
}
@Override
public void putByPath(String expression, Object value) {
BeanPath.create(expression).set(this, value);
@ -561,7 +568,7 @@ public class JSONObject extends MapWrapper<String, Object> implements JSON, JSON
*/
public Writer write(Writer writer, int indentFactor, int indent, Filter<MutablePair<Object, Object>> filter) throws JSONException {
final JSONWriter jsonWriter = JSONWriter.of(writer, indentFactor, indent, config)
.beginObj();
.beginObj();
this.forEach((key, value) -> jsonWriter.writeField(new MutablePair<>(key, value), filter));
jsonWriter.end();
// 此处不关闭Writer考虑writer后续还需要填内容

View File

@ -0,0 +1,38 @@
package cn.hutool.json;
import cn.hutool.json.serialize.GlobalSerializeMapping;
import cn.hutool.json.serialize.JSONObjectSerializer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class IssueIB9MH0Test {
@Test
void parseTest() {
GlobalSerializeMapping.put(TabTypeEnum.class, (JSONObjectSerializer<TabTypeEnum>) (json, bean) -> json.set("code", bean.getCode())
.set("title", bean.getTitle()));
final JSON parse = JSONUtil.parse(TabTypeEnum._01);
Assertions.assertEquals("{\"code\":\"tab_people_home\",\"title\":\"首页\"}", parse.toString());
}
public enum TabTypeEnum {
_01("tab_people_home","首页"),
_02("tab_people_hospital","医院");
private String code;
private String title;
TabTypeEnum(String code, String title) {
this.code = code;
this.title = title;
}
public String getCode() {
return code;
}
public String getTitle() {
return title;
}
}
}

View File

@ -1,8 +1,12 @@
package cn.hutool.json;
import static org.junit.jupiter.api.Assertions.*;
import cn.hutool.core.lang.TypeReference;
import org.junit.jupiter.api.Test;
import java.util.List;
/**
* JSON路径单元测试
*
@ -27,4 +31,23 @@ public class JSONPathTest {
Long accountId = JSONUtil.getByPath(json, "$.accountId", 0L);
assertEquals(111L, accountId.longValue());
}
@Test
public void getByPathTest3(){
String str = "[{'accountId':1},{'accountId':2},{'accountId':3}]";
JSON json = JSONUtil.parse(str);
// 返回指定泛型的对象 List<Long>
List<Long> accountIds = json.getByPath("$.accountId", new TypeReference<List<Long>>() {
});
assertNotNull(accountIds);
assertArrayEquals(new Long[]{1L, 2L, 3L}, accountIds.toArray());
str = "{\"accountInfos\": [{\"accountId\":1},{\"accountId\":2},{\"accountId\":3}]}";
json = JSONUtil.parse(str);
// 返回指定泛型的对象 List<Long>
accountIds = json.getByPath("$.accountInfos.accountId", new TypeReference<List<Long>>() {
});
assertNotNull(accountIds);
assertArrayEquals(new Long[]{1L, 2L, 3L}, accountIds.toArray());
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-jwt</artifactId>

View File

@ -278,7 +278,7 @@ public class JWTSignerUtil {
if (key instanceof PrivateKey || key instanceof PublicKey) {
// issue3205@Github
if(ReUtil.isMatch("ES\\d{3}", algorithmId)){
return new EllipticCurveJWTSigner(algorithmId, key);
return new EllipticCurveJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key);
}
return new AsymmetricJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key);

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-log</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-poi</artifactId>

View File

@ -865,11 +865,20 @@ public class ExcelWriter extends ExcelBase<ExcelWriter> {
boolean isFirstRow = true;
Map<?, ?> map;
for (Object obj : data) {
if (obj instanceof Map) {
map = new TreeMap<>(comparator);
map.putAll((Map) obj);
// 只第一行使用比较器排序
if (isFirstRow) {
if (obj instanceof Map) {
map = new TreeMap<>(comparator);
map.putAll((Map) obj);
} else {
map = BeanUtil.beanToMap(obj, new TreeMap<>(comparator), false, false);
}
} else {
map = BeanUtil.beanToMap(obj, new TreeMap<>(comparator), false, false);
if (obj instanceof Map) {
map = (Map) obj;
} else {
map = BeanUtil.beanToMap(obj, new HashMap<>(), false, false);
}
}
writeRow(map, isFirstRow);
if (isFirstRow) {

View File

@ -147,6 +147,8 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
factory.processWorkbookEvents(request, fs);
} catch (IOException e) {
throw new POIException(e);
} catch (final StopReadException e) {
// issue#3820 跳过用户抛出此异常表示强制结束读取
} finally {
IoUtil.close(fs);
}

View File

@ -47,8 +47,8 @@ public class ExcelSaxUtil {
*/
public static ExcelSaxReader<?> createSaxReader(boolean isXlsx, RowHandler rowHandler) {
return isXlsx
? new Excel07SaxReader(rowHandler)
: new Excel03SaxReader(rowHandler);
? new Excel07SaxReader(rowHandler)
: new Excel03SaxReader(rowHandler);
}
/**
@ -184,6 +184,8 @@ public class ExcelSaxUtil {
throw new IORuntimeException(e);
} catch (SAXException e) {
throw new POIException(e);
} catch (final StopReadException e) {
// issue#3820 跳过用户抛出此异常表示强制结束读取
}
}
@ -268,7 +270,7 @@ public class ExcelSaxUtil {
// issue#IB0EJ9 可能精度丢失对含有小数的value判断并转为BigDecimal
final double number = Double.parseDouble(value);
if(StrUtil.contains(value, CharUtil.DOT) && !value.equals(Double.toString(number))){
if (StrUtil.contains(value, CharUtil.DOT) && !value.equals(Double.toString(number))) {
// 精度丢失
return NumberUtil.toBigDecimal(value);
}

View File

@ -0,0 +1,33 @@
package cn.hutool.poi.excel.sax;
import cn.hutool.poi.exceptions.POIException;
/**
* 读取结束异常用于标记读取结束<br>
* Sax方式读取时如果用户在RowHandler中抛出此异常表示读取结束此时不再读取其他数据
*
* @author Looly
* @since 5.8.35
*/
public class StopReadException extends POIException {
private static final long serialVersionUID = 1L;
/**
* 构造
*
*/
public StopReadException() {
this("Stop read by user.");
}
/**
* 构造
*
* @param message 消息
*/
public StopReadException(final String message) {
super(message);
// 去除堆栈
setStackTrace(new StackTraceElement[0]);
}
}

View File

@ -8,6 +8,7 @@ import cn.hutool.core.lang.Console;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.cell.FormulaCellValue;
import cn.hutool.poi.excel.sax.Excel03SaxReader;
import cn.hutool.poi.excel.sax.StopReadException;
import cn.hutool.poi.excel.sax.handler.RowHandler;
import cn.hutool.poi.exceptions.POIException;
import org.apache.poi.ss.usermodel.CellStyle;
@ -33,6 +34,24 @@ public class ExcelSaxReadTest {
ExcelUtil.readBySax("aaa.xlsx", 0, createRowHandler());
}
@Test
void readEndByExceptionTest(){
ExcelUtil.readBySax("aaa.xlsx", 0, (sheetIndex, rowIndex, rowList) -> {
if (rowIndex == 1) {
throw new StopReadException();
}
});
}
@Test
void readEndByException03Test(){
ExcelUtil.readBySax("aaa.xls", 0, (sheetIndex, rowIndex, rowList) -> {
if (rowIndex == 1) {
throw new StopReadException();
}
});
}
@Test
public void excel07ByNameTest() {
// 工具化快速读取

View File

@ -2,6 +2,7 @@ package cn.hutool.poi.excel;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.comparator.IndexedComparator;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Console;
@ -904,4 +905,75 @@ public class ExcelWriteTest {
}
}
@Test
@Disabled
public void writeWithComparatorTest() {
// 生成测试数据, 10w行50列
List<Map<String, Object>> dataList = new ArrayList<>();
for (int i = 1; i <= 100000; i++) {
Map<String, Object> map = new HashMap<>();
map.put("test11", "test11_" + i);
map.put("test12", "test12_" + i);
map.put("test13", "test13_" + i);
map.put("test14", "test14_" + i);
map.put("test15", "test15_" + i);
map.put("test16", "test16_" + i);
map.put("test17", "test17_" + i);
map.put("test18", "test18_" + i);
map.put("test19", "test19_" + i);
map.put("test1", "test1_" + i);
map.put("test2", "test2_" + i);
map.put("test3", "test3_" + i);
map.put("test4", "test4_" + i);
map.put("test5", "test5_" + i);
map.put("test6", "test6_" + i);
map.put("test7", "test7_" + i);
map.put("test8", "test8_" + i);
map.put("test9", "test9_" + i);
map.put("test10", "test10_" + i);
map.put("test20", "test20_" + i);
map.put("test21", "test21_" + i);
map.put("test22", "test22_" + i);
map.put("test23", "test23_" + i);
map.put("test24", "test24_" + i);
map.put("test25", "test25_" + i);
map.put("test26", "test26_" + i);
map.put("test27", "test27_" + i);
map.put("test28", "test28_" + i);
map.put("test29", "test29_" + i);
map.put("test30", "test30_" + i);
map.put("test41", "test41_" + i);
map.put("test42", "test42_" + i);
map.put("test43", "test43_" + i);
map.put("test44", "test44_" + i);
map.put("test45", "test45_" + i);
map.put("test46", "test46_" + i);
map.put("test47", "test47_" + i);
map.put("test48", "test48_" + i);
map.put("test49", "test49_" + i);
map.put("test50", "test50_" + i);
map.put("test31", "test31_" + i);
map.put("test32", "test32_" + i);
map.put("test33", "test33_" + i);
map.put("test34", "test34_" + i);
map.put("test35", "test35_" + i);
map.put("test36", "test36_" + i);
map.put("test37", "test37_" + i);
map.put("test38", "test38_" + i);
map.put("test39", "test39_" + i);
map.put("test40", "test40_" + i);
dataList.add(map);
}
// 使用比较器写出
try (BigExcelWriter excelWriter = ExcelUtil.getBigWriter("d:/writeWithComparatorTest.xlsx")) {
excelWriter.write(dataList, new IndexedComparator<>(
"test1", "test2", "test3", "test4", "test5", "test6", "test7", "test8", "test9", "test10",
"test11", "test12", "test13", "test14", "test15", "test16", "test17", "test18", "test19", "test20",
"test21", "test22", "test23", "test24", "test25", "test26", "test27", "test28", "test29", "test30",
"test31", "test32", "test33", "test34", "test35", "test36", "test37", "test38", "test39", "test40",
"test41", "test42", "test43", "test44", "test45", "test46", "test47", "test48", "test49", "test50"
));
}
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-script</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-setting</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-socket</artifactId>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
</parent>
<artifactId>hutool-system</artifactId>

View File

@ -8,7 +8,7 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.34</version>
<version>5.8.35</version>
<name>hutool</name>
<description>Hutool是一个小而全的Java工具类库通过静态方法封装降低相关API的学习成本提高工作效率使Java拥有函数式语言般的优雅让Java语言也可以“甜甜的”。</description>
<url>https://github.com/dromara/hutool</url>