Compare commits

...

89 Commits

Author SHA1 Message Date
chinabugotech
4656e8d3ec
update CHANGELOG.md.
Signed-off-by: chinabugotech <bugo@bugotech.cn>
2025-12-01 02:39:11 +00:00
chinabugotech
3a96690070
update CHANGELOG.md.
Signed-off-by: chinabugotech <bugo@bugotech.cn>
2025-12-01 02:38:50 +00:00
chinabugotech
f4e5a10697 🚀release5.8.42 2025-12-01 09:52:44 +08:00
Looly
6802979215
!1398 fix(方法注释):StrUtil类中方法注释问题修复
Merge pull request !1398 from fanzhenyu/v5-dev
2025-12-01 01:20:15 +00:00
fanzhenyu
662cb9aad1 fix(方法注释):StrUtil类中方法注释问题修复 2025-11-30 23:09:28 +08:00
Looly
6bb750fb2c add test 2025-11-30 21:07:49 +08:00
Looly
95c8e73c16 修复HexUtil.format在处理长度小于2的字符串会抛异常,在处理长度为奇数的字符串时最后一个字符会被忽略的问题 修复SplitIter.computeNext递归调用可能导致栈溢出风险 2025-11-30 16:51:31 +08:00
Golden Looly
af97ba4084
Merge pull request #4168 from TouyamaRie/v5-dev-1128
Fix issue 4167
2025-11-30 16:47:40 +08:00
Looly
8afee460fd fix doc 2025-11-30 16:21:22 +08:00
TouyamaRie
0f60aa021a Fix issue 4169 2025-11-28 17:15:18 +08:00
TouyamaRie
d821c410c6 Fix issue 4167 2025-11-28 16:04:46 +08:00
Looly
882f3923ce ArrangementTest 2025-11-28 09:56:53 +08:00
Golden Looly
1d9b0ebbd2
Merge pull request #4166 from qieugxi/hutool-1127-4
fix issue 4165
2025-11-28 09:55:18 +08:00
ZWM2
9191e49c21 fix issue 4165 2025-11-27 20:09:26 +08:00
Looly
a56d2c03dc fix code 2025-11-26 23:35:28 +08:00
Looly
d9cbb19460 修复Word07Writerrun.setColor()的颜色十六进制转换逻辑(pr#4164@Github) 2025-11-26 20:33:56 +08:00
Golden Looly
f18a2b512f
Merge pull request #4164 from CherryRum/fix/set-color-argb
fix(Word07Writer): strip alpha channel from color and update tests
2025-11-26 20:32:48 +08:00
yulin
6fcd1a2603 fix(Word07Writer): strip alpha channel from color and update tests 2025-11-26 20:23:09 +08:00
Looly
5ae493119f fix doc 2025-11-26 20:05:44 +08:00
Looly
ea717843f6 fix doc 2025-11-26 20:04:48 +08:00
Looly
b8908cf3ef 增强HexUtil自动去除0x#前缀(pr#4163@Github) 2025-11-26 20:02:12 +08:00
Golden Looly
ab1774d4e6
Merge pull request #4163 from sunshineflymeat/hutool-1126-3
Fix issue 4162
2025-11-26 19:57:15 +08:00
Looly
ccaecf6bc9 Word07Writer增加addText重载,支持字体颜色(pr#1388@Gitee) 2025-11-26 19:44:34 +08:00
Looly
3d2dd38add
!1388 feat:Word生成器,增加段落新增字体颜色参数
Merge pull request !1388 from liyong473/v5-dev
2025-11-26 11:41:26 +00:00
Looly
d64a0d36aa 修复DateModifier处理AM和PM的ceiling和round问题(pr#4161@Github) 2025-11-26 17:24:16 +08:00
Golden Looly
ab936327a2
Merge pull request #4161 from asukavuuyn/v5-dev
fix:12小时制ceiling和round问题
2025-11-26 17:22:07 +08:00
liyong
6ef5f1c1fd feat:Word生成器,增加段落新增字体颜色参数 2025-11-26 17:16:51 +08:00
ZWM
6776ddb29d Fix issue 4162 2025-11-26 17:11:18 +08:00
Looly
449df10509 修复ReflectUtil.newInstanceIfPossible传入Object逻辑错误(pr#4160@Github) 2025-11-26 16:30:34 +08:00
asukavuuyn
fc5e1ecff9 fix:12小时制ceiling和round问题 2025-11-26 15:19:41 +08:00
Looly
472b0d2841 优化ObjectUtil.containsString改为CharSequence(pr#4154@Github) 2025-11-26 09:43:25 +08:00
Golden Looly
00748130ef
Merge pull request #4155 from IcoreE/v5-dev_1125
fix issues 4154
2025-11-26 09:41:25 +08:00
yanzhongxin
820e0d0c8b 修复ObjectUtil.contains处理字符串类型不合理 2025-11-25 21:26:08 +08:00
Looly
86c3c530b6 修复StrUtil.str(ByteBuffer, Charset) 方法修改入参 ByteBufferposition,导致入参变化 (pr#4153@Github) 2025-11-25 18:54:13 +08:00
Looly
2a08f40527 BufferUtilTest 2025-11-25 18:53:23 +08:00
Golden Looly
a8cc614fa6
Merge pull request #4153 from ZhonglinGui/v5-dev
修复StrUtil.str(ByteBuffer, Charset) 方法修改入参 ByteBuffer 的 position,导致入参变化
2025-11-25 18:52:00 +08:00
Will
a6151b6ee8 修复StrUtil.str(ByteBuffer, Charset) 方法修改入参 ByteBuffer 的 position,导致入参变化 2025-11-25 18:08:03 +08:00
Looly
3c067f1871 优化EscapeUtil,兼容不规范的转义(pr#4150@Github) 2025-11-25 17:07:17 +08:00
Looly
654db66ecb fix pr#4149@Github 2025-11-25 15:16:42 +08:00
Looly
bd42686a65 CombinationArrangement 重构避免数组频繁拷贝,并避免溢出(pr#4144@Github) 2025-11-24 16:05:43 +08:00
Golden Looly
e8eec73125
Merge pull request #4144 from CherryRum/fix-combination
Enhance Mathematical Correctness and Performance in Combination and Arrangement Modules
2025-11-24 16:04:03 +08:00
yulin
04314cdd4a fix(Arrangement): improve count and select methods with overflow checks and enhance documentation 2025-11-23 23:37:30 +08:00
Looly
4d04d5daf9 修复TypeUtil.getClass无法识别GenericArrayType问题(pr#4138@Github) 2025-11-23 23:27:45 +08:00
Golden Looly
1a7d522dff
Merge pull request #4138 from sunshineflymeat/hutool-1120
Fix issue 4137
2025-11-23 23:23:10 +08:00
Looly
eec6056876 修复FileNameUtil.extName在特殊后缀判断逻辑过于宽松导致误判问题(pr#4142@Github) 2025-11-23 23:17:01 +08:00
Golden Looly
c6cbaeabff
Merge pull request #4142 from ZhonglinGui/v5-dev
修复FileNameUtil.extName 方法对特殊后缀判断逻辑过于宽松导致误判
2025-11-23 23:14:42 +08:00
Golden Looly
9dad6fc4d5
Merge pull request #4143 from CherryRum/v5-dev
修改部分描述
2025-11-23 23:11:00 +08:00
yulin
5e1110426b feat(Combination): add BigInteger-based combination calculation methods and deprecate old count method 2025-11-23 22:41:39 +08:00
yulin
edb4401e47 fix(LocalPortGenerater): update class documentation and plan for future name correction 2025-11-23 19:26:36 +08:00
yulin
7258a5b946 fix(URLUtil): correct spelling of "occurred" in exception messages 2025-11-23 19:12:35 +08:00
Will
17f78f8cd4 修复FileNameUtil.extName 方法对特殊后缀判断逻辑过于宽松导致误判 2025-11-21 14:42:07 +08:00
zwm
ed27c637b2 feat: 解决TypeUtil.getClass方法传入泛型数组类型时方法返回错误值null问题 2025-11-20 23:18:42 +08:00
Looly
ff32ed0872 修复Validator.isBetween在高精度Number类型下存在精度丢失问题(pr#4136@Github) 2025-11-20 10:52:08 +08:00
Golden Looly
9f6d3b5430
Merge pull request #4136 from ZhonglinGui/v5-dev
修复Validator.isBetween方法在高精度Number类型下存在精度丢失问题
2025-11-20 10:49:03 +08:00
Will
f9eb29fa87 修复Validator.isBetween方法在高精度Number类型下存在精度丢失问题 2025-11-20 10:34:06 +08:00
Looly
8003c4416a 修复VersionUtil.matchEl如果输入的版本范围表达式右边界为空时,会抛出数组越界访问错误的问题(pr#4130@Github) 2025-11-17 22:19:07 +08:00
Golden Looly
c84ec20f5a
Merge pull request #4130 from sunshineflymeat/hutool-1114
Fix issue 4129
2025-11-17 22:16:19 +08:00
Looly
b0c3350ef7 修复ImgUtil.write没有释放BufferedImage可能导致内存泄露(issue#ID6VNJ@Gitee) 2025-11-17 21:31:50 +08:00
zwm
44a2afd588 feat:修复VersionUtil.matchEl方法中版本范围表达式右边界为空时数组越界访问错误 2025-11-14 17:22:01 +08:00
Looly
41307a6e3d 修复NumberWithFormat没有实现Comparable接口导致的JSON排序报错问题(issue#ID61QR@Gitee) 2025-11-12 22:22:29 +08:00
Looly
9b3414b397 add test and fix comment 2025-11-12 22:09:53 +08:00
Looly
f60f20243d update dependency 2025-11-04 15:23:26 +08:00
Looly
3b15ae08ae 修复FileUtil.listFileNames相对路径index混乱问题(issue#4121@Github) 2025-10-29 23:12:02 +08:00
Looly
62f04b2c0d 修复JsonUtil.toJsonStr对Boolean和Number返回错误问题(issue#4109@Github)。 2025-10-29 18:50:10 +08:00
Looly
cbade4e239 fix comment issue#ID428M 2025-10-29 18:41:23 +08:00
Looly
edeb87c7f4 修复HttpConnection.reflectSetMethod反射在JDK9+权限问题(issue#4109@Github) 2025-10-28 18:23:22 +08:00
Looly
a3e58451fc change link 2025-10-27 15:50:45 +08:00
Looly
02988f3714 change link 2025-10-27 15:43:59 +08:00
Looly
0cbc36e225 add test 2025-10-24 21:11:42 +08:00
Looly
ce3d69dd35 add test 2025-10-24 18:49:46 +08:00
Looly
4162c519b7 修复PasswdStrength.checkindexOf逻辑问题(pr#4114@Github)。 2025-10-24 16:00:20 +08:00
Golden Looly
b136d81720
Merge pull request #4114 from sunshineflymeat/hutool-1024
feat:修复PasswdStrength.check方法检测密码强度等级逻辑有误问题
2025-10-24 15:48:06 +08:00
zwm
8469fd0c49 feat:修复PasswdStrength.check方法检测密码强度等级逻辑有误问题 2025-10-24 13:53:22 +08:00
Looly
e9c4e65f97 修复Dialect.psForCount未传入Wrapper导致大小写问题(issue#ID39G9@Gitee)。 2025-10-23 21:51:13 +08:00
Looly
4c563da8bd 修复JschSessionPool.remove逻辑错误问题。 2025-10-23 11:16:12 +08:00
Looly
fb95caa7b9 增加代理支持(pr#4107@Github) 2025-10-23 02:52:56 +08:00
Golden Looly
25839055d6
Merge pull request #4107 from HeyAlaia/v5-dev
添加AI代理设置
2025-10-23 02:44:40 +08:00
Alaia
afc1036fb6 fix: ai配置支持进行代理配置 2025-10-22 17:44:59 +08:00
Alaia
7d84d1a81c fix: 修复字符串null 2025-10-22 16:39:36 +08:00
Looly
5a3ad06601 增加JakartaSoapClient(issue#4103@Github) 2025-10-22 00:56:51 +08:00
Looly
47c48b23b3 修复verify方法在定义alg为none时验证失效问题(issue#4105@Github) 2025-10-22 00:47:14 +08:00
Looly
dcdba8314d 修复verify方法在定义alg为none时验证失效问题(issue#4105@Github) 2025-10-22 00:05:58 +08:00
Looly
97e56c48eb ListUtil增加zip方法(pr#4052@Github) 2025-10-18 19:39:53 +08:00
Golden Looly
ddd3eb34f6
Merge pull request #4102 from liunainaibuheliulianniunai/v5-dev
增加zip函数,可以将两个列表中的元素一一配对并返回一个新的结果列表
2025-10-18 19:24:34 +08:00
刘奶奶不喝榴莲牛奶
4a88a565bf 增加zip函数,可以将两个列表中的元素一一配对并返回一个新的结果列表 2025-10-18 12:52:49 +08:00
Looly
9d0b6d652f fix readme 2025-10-15 16:40:14 +08:00
Looly
fa238bc4c9 add test 2025-10-15 11:31:23 +08:00
chinabugotech
29902b9093
update CHANGELOG.md.
Signed-off-by: chinabugotech <bugo@bugotech.cn>
2025-10-13 06:13:57 +00:00
chinabugotech
da2d0823b9 🐢prepare5.8.42 2025-10-13 14:01:20 +08:00
103 changed files with 3119 additions and 371 deletions

View File

@ -1,5 +1,44 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
# 5.8.42(2025-11-28)
### 🐣新特性
* 【core 】 `ListUtil`增加`zip`方法pr#4052@Github
* 【http 】 增加`JakartaSoapClient`issue#4103@Github
* 【ai 】 增加代理支持pr#4107@Github
* 【core 】 `CharSequenceUtil`增加`builder`方法重载pr#4107@Github
* 【core 】 `Combination``Arrangement `重构避免数组频繁拷贝并避免溢出pr#4144@Github
* 【core 】 优化`EscapeUtil`兼容不规范的转义pr#4150@Github
* 【core 】 优化`ObjectUtil.contains`String改为CharSequencepr#4154@Github
* 【poi 】 `Word07Writer`增加addText重载支持字体颜色pr#1388@Gitee
* 【core 】 增强`HexUtil`自动去除`0x``#`前缀pr#4163@Github
### 🐞Bug修复
* 【jwt 】 修复verify方法在定义alg为`none`时验证失效问题issue#4105@Github
* 【extra 】 修复`JschSessionPool.remove`逻辑错误问题issue#ID4XZ7@gitee
* 【db 】 修复`Dialect.psForCount`未传入Wrapper导致大小写问题issue#ID39G9@Gitee
* 【core 】 修复`PasswdStrength.check`indexOf逻辑问题pr#4114@Github
* 【http 】 修复`HttpConnection.reflectSetMethod`反射在JDK9+权限问题issue#4109@Github
* 【http 】 修复`JsonUtil.toJsonStr`对Boolean和Number返回错误问题issue#4109@Github
* 【core 】 修复`FileUtil.listFileNames`相对路径index混乱问题issue#4121@Github
* 【core 】 修复`NumberWithFormat`没有实现Comparable接口导致的JSON排序报错问题issue#ID61QR@Gitee
* 【core 】 修复`ImgUtil.write`没有释放BufferedImage可能导致内存泄露issue#ID6VNJ@Gitee
* 【core 】 修复`VersionUtil.matchEl`如果输入的版本范围表达式右边界为空时会抛出数组越界访问错误的问题pr#4130@Github
* 【core 】 修复`Validator.isBetween`在高精度Number类型下存在精度丢失问题pr#4136@Github
* 【core 】 修复`FileNameUtil.extName`在特殊后缀判断逻辑过于宽松导致误判问题pr#4142@Github
* 【core 】 修复`TypeUtil.getClass`无法识别`GenericArrayType`问题pr#4138@Github
* 【core 】 修复`CreditCodeUtil.randomCreditCode`部分字母未使用问题pr#4149@Github
* 【core 】 修复`CacheableAnnotationAttribute`可能并发问题pr#4149@Github
* 【core 】 修复`URLUtil.url`未断开连接问题pr#4149@Github
* 【core 】 修复`Bimap.put`重复put问题pr#4150@Github
* 【core 】 修复`StrUtil.str(ByteBuffer, Charset)` 方法修改入参 `ByteBuffer``position`,导致入参变化 pr#4153@Github
* 【core 】 修复`ReflectUtil.newInstanceIfPossible`传入Object逻辑错误pr#4160@Github
* 【core 】 修复`DateModifier`处理AM和PM的ceiling和round问题pr#4161@Github
* 【poi 】 修复`Word07Writer`run.setColor()的颜色十六进制转换逻辑pr#4164@Github
* 【core 】 修复`Arrangement.iterate(int m)`方法的排列迭代器实现逻辑问题pr#4166@Github
* 【core 】 修复`HexUtil.format`在处理长度小于2的字符串会抛异常在处理长度为奇数的字符串时最后一个字符会被忽略的问题pr#4168@Github
* 【core 】 修复`SplitIter.computeNext`递归调用可能导致栈溢出风险pr#4168@Github
-------------------------------------------------------------------------------------------------------------
# 5.8.41(2025-10-12)

View File

@ -18,8 +18,8 @@
<a target="_blank" href="https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html">
<img src="https://img.shields.io/badge/JDK-8+-green.svg" />
</a>
<a target="_blank" href="https://travis-ci.com/chinabugotech/hutool">
<img src="https://travis-ci.com/chinabugotech/hutool.svg?branch=v5-master" />
<a target="_blank" href="https://app.travis-ci.com/chinabugotech/hutool">
<img src="https://api.travis-ci.com/chinabugotech/hutool.svg?branch=v5-master" />
</a>
<a href="https://www.codacy.com/gh/chinabugotech/hutool/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=chinabugotech/hutool&amp;utm_campaign=Badge_Grade">
<img src="https://app.codacy.com/project/badge/Grade/8a6897d9de7440dd9de8804c28d2871d"/>
@ -49,8 +49,6 @@
-------------------------------------------------------------------------------
[**🌎中文说明**](README.md)
-------------------------------------------------------------------------------
@ -134,18 +132,18 @@ Each module can be introduced individually, or all modules can be introduced by
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.41</version>
<version>5.8.42</version>
</dependency>
```
### 🍐Gradle
```
implementation 'cn.hutool:hutool-all:5.8.41'
implementation 'cn.hutool:hutool-all:5.8.42'
```
## 📥Download
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.41/)
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.42/)
> 🔔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.

View File

@ -18,8 +18,8 @@
<a target="_blank" href="https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html">
<img src="https://img.shields.io/badge/JDK-8+-green.svg" />
</a>
<a target="_blank" href="https://travis-ci.com/chinabugotech/hutool">
<img src="https://travis-ci.com/chinabugotech/hutool.svg?branch=v5-master" />
<a target="_blank" href="https://app.travis-ci.com/chinabugotech/hutool">
<img src="https://api.travis-ci.com/chinabugotech/hutool.svg?branch=v5-master" />
</a>
<a href="https://www.codacy.com/gh/chinabugotech/hutool/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=chinabugotech/hutool&amp;utm_campaign=Badge_Grade">
<img src="https://app.codacy.com/project/badge/Grade/8a6897d9de7440dd9de8804c28d2871d"/>
@ -46,8 +46,6 @@
-------------------------------------------------------------------------------
=======
[**🌎English Documentation**](README-EN.md)
-------------------------------------------------------------------------------
@ -124,20 +122,20 @@ Hutool = Hu + tool是原公司项目底层代码剥离后的开源库“Hu
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.41</version>
<version>5.8.42</version>
</dependency>
```
### 🍐Gradle
```
implementation 'cn.hutool:hutool-all:5.8.41'
implementation 'cn.hutool:hutool-all:5.8.42'
```
### 📥下载jar
点击以下链接,下载`hutool-all-X.X.X.jar`即可:
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.41/)
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.42/)
> 🔔️注意
> Hutool 5.x支持JDK8+对Android平台没有测试不能保证所有工具类或工具方法可用。

View File

@ -1 +1 @@
5.8.41
5.8.42

View File

@ -1 +1 @@
var version = '5.8.41'
var version = '5.8.42'

View File

@ -6,7 +6,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.41</version>
<version>5.8.42</version>
</parent>
<artifactId>hutool-ai</artifactId>

View File

@ -16,6 +16,7 @@
package cn.hutool.ai.core;
import java.net.Proxy;
import java.util.Map;
/**
@ -142,4 +143,36 @@ public interface AIConfig {
*/
int getReadTimeout();
/**
* 获取是否使用代理
*
* @return hasProxy
* @since 5.8.42
*/
boolean getHasProxy();
/**
* 设置是否使用代理
*
* @param hasProxy 是否使用代理
* @since 5.8.42
*/
void setHasProxy(boolean hasProxy);
/**
* 获取代理配置
*
* @return proxy
* @since 5.8.42
*/
Proxy getProxy();
/**
* 设置代理配置
*
* @param proxy 连接超时时间
* @since 5.8.42
*/
void setProxy(Proxy proxy);
}

View File

@ -17,6 +17,7 @@
package cn.hutool.ai.core;
import java.lang.reflect.Constructor;
import java.net.Proxy;
/**
* 用于AIConfig的创建创建同时支持链式设置参数
@ -160,6 +161,21 @@ public class AIConfigBuilder {
return this;
}
/**
* 设置代理
*
* @param proxy 取超时时间
* @return config
* @since 5.8.42
*/
public synchronized AIConfigBuilder setProxy(final Proxy proxy) {
if (null != proxy) {
config.setHasProxy(true);
config.setProxy(proxy);
}
return this;
}
/**
* 返回config实例
*

View File

@ -28,6 +28,8 @@ import java.net.URL;
import java.util.Map;
import java.util.function.Consumer;
import static cn.hutool.core.thread.GlobalThreadPool.execute;
/**
* 基础AIService包含基公共参数和公共方法
*
@ -56,11 +58,14 @@ public class BaseAIService {
//链式构建请求
try {
//设置超时3分钟
return HttpRequest.get(config.getApiUrl() + endpoint)
HttpRequest httpRequest = HttpRequest.get(config.getApiUrl() + endpoint)
.header(Header.ACCEPT, "application/json")
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
.timeout(config.getTimeout())
.execute();
.timeout(config.getTimeout());
if (config.getHasProxy()) {
httpRequest.setProxy(config.getProxy());
}
return httpRequest.execute();
} catch (final AIException e) {
throw new AIException("Failed to send GET request: " + e.getMessage(), e);
}
@ -75,13 +80,16 @@ public class BaseAIService {
protected HttpResponse sendPost(final String endpoint, final String paramJson) {
//链式构建请求
try {
return HttpRequest.post(config.getApiUrl() + endpoint)
HttpRequest httpRequest = HttpRequest.post(config.getApiUrl() + endpoint)
.header(Header.CONTENT_TYPE, "application/json")
.header(Header.ACCEPT, "application/json")
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
.body(paramJson)
.timeout(config.getTimeout())
.execute();
.timeout(config.getTimeout());
if (config.getHasProxy()) {
httpRequest.setProxy(config.getProxy());
}
return httpRequest.execute();
} catch (final AIException e) {
throw new AIException("Failed to send POST request" + e.getMessage(), e);
}
@ -98,13 +106,16 @@ public class BaseAIService {
//链式构建请求
try {
//设置超时3分钟
return HttpRequest.post(config.getApiUrl() + endpoint)
HttpRequest httpRequest = HttpRequest.post(config.getApiUrl() + endpoint)
.header(Header.CONTENT_TYPE, "multipart/form-data")
.header(Header.ACCEPT, "application/json")
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
.form(paramMap)
.timeout(config.getTimeout())
.execute();
.timeout(config.getTimeout());
if (config.getHasProxy()) {
httpRequest.setProxy(config.getProxy());
}
return httpRequest.execute();
} catch (final AIException e) {
throw new AIException("Failed to send POST request" + e.getMessage(), e);
}
@ -123,6 +134,9 @@ public class BaseAIService {
// 创建连接
URL apiUrl = new URL(config.getApiUrl() + endpoint);
connection = (HttpURLConnection) apiUrl.openConnection();
if (config.getHasProxy()) {
connection = (HttpURLConnection) apiUrl.openConnection(config.getProxy());
}
connection.setRequestMethod(Method.POST.name());
connection.setRequestProperty(Header.CONTENT_TYPE.getValue(), "application/json");
connection.setRequestProperty(Header.AUTHORIZATION.getValue(), "Bearer " + config.getApiKey());

View File

@ -16,6 +16,7 @@
package cn.hutool.ai.core;
import java.net.Proxy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -39,6 +40,10 @@ public class BaseConfig implements AIConfig {
protected volatile int timeout = 180000;
//读取超时时间
protected volatile int readTimeout = 300000;
//是否设置代理
protected volatile boolean hasProxy = false;
//代理设置
protected volatile Proxy proxy;
@Override
public void setApiKey(final String apiKey) {
@ -104,4 +109,24 @@ public class BaseConfig implements AIConfig {
public void setReadTimeout(final int readTimeout) {
this.readTimeout = readTimeout;
}
@Override
public boolean getHasProxy() {
return hasProxy;
}
@Override
public void setHasProxy(boolean hasProxy) {
this.hasProxy = hasProxy;
}
@Override
public Proxy getProxy() {
return proxy;
}
@Override
public void setProxy(Proxy proxy) {
this.proxy = proxy;
}
}

View File

@ -0,0 +1,253 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.ai.model.openai;
import cn.hutool.ai.AIServiceFactory;
import cn.hutool.ai.ModelName;
import cn.hutool.ai.Models;
import cn.hutool.ai.core.AIConfigBuilder;
import cn.hutool.ai.core.Message;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class OpenaiProxyServiceTest {
String key = "your key";
//you proxy hostname
String hostname = "you proxy hostname";
//you proxy port
int port = 7890;
OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()).setApiKey(key).setProxy(new Proxy(Proxy.Type.HTTP,
new InetSocketAddress(hostname, port))).build(), OpenaiService.class);
@Test
@Disabled
void chat(){
final String chat = openaiService.chat("写一个疯狂星期四广告词");
assertNotNull(chat);
}
@Test
@Disabled
void chatStream() {
String prompt = "写一个疯狂星期四广告词";
// 使用AtomicBoolean作为结束标志
AtomicBoolean isDone = new AtomicBoolean(false);
openaiService.chat(prompt, data -> {
assertNotNull(data);
if (data.contains("[DONE]")) {
// 设置结束标志
isDone.set(true);
} else if (data.contains("\"error\"")) {
isDone.set(true);
}
});
// 轮询检查结束标志
while (!isDone.get()) {
ThreadUtil.sleep(100);
}
}
@Test
@Disabled
void testChat(){
final List<Message> messages = new ArrayList<>();
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
messages.add(new Message("user","给我说一个笑话"));
final String chat = openaiService.chat(messages);
assertNotNull(chat);
}
@Test
@Disabled
void chatVision() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);
final String chatVision = openaiService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544","https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800"));
assertNotNull(chatVision);
}
@Test
@Disabled
void testChatVisionStream() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);
String prompt = "图片上有些什么?";
List<String> images = Collections.singletonList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800");
// 使用AtomicBoolean作为结束标志
AtomicBoolean isDone = new AtomicBoolean(false);
openaiService.chatVision(prompt,images, data -> {
assertNotNull(data);
if (data.contains("[DONE]")) {
// 设置结束标志
isDone.set(true);
} else if (data.contains("\"error\"")) {
isDone.set(true);
}
});
// 轮询检查结束标志
while (!isDone.get()) {
ThreadUtil.sleep(100);
}
}
@Test
@Disabled
void imagesGenerations() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.DALL_E_3.getModel()).build(), OpenaiService.class);
final String imagesGenerations = openaiService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
assertNotNull(imagesGenerations);
//https://oaidalleapiprodscus.blob.core.windows.net/private/org-l99H6T0zCZejctB2TqdYrXFB/user-LilDVU1V8cUxJYwVAGRkUwYd/img-yA9kNatHnBiUHU5lZGim1hP2.png?st=2025-03-07T01%3A04%3A18Z&se=2025-03-07T03%3A04%3A18Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=d505667d-d6c1-4a0a-bac7-5c84a87759f8&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-03-06T15%3A04%3A42Z&ske=2025-03-07T15%3A04%3A42Z&sks=b&skv=2024-08-04&sig=rjcRzC5U7Y3pEDZ4ME0CiviAPdIpoGO2rRTXw3m8rHw%3D
}
@Test
@Disabled
void imagesEdits() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);
final File file = FileUtil.file("your imgUrl");
final String imagesEdits = openaiService.imagesEdits("茂密的森林中,有一只九色鹿若隐若现",file);
assertNotNull(imagesEdits);
}
@Test
@Disabled
void imagesVariations() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);
final File file = FileUtil.file("your imgUrl");
final String imagesVariations = openaiService.imagesVariations(file);
assertNotNull(imagesVariations);
}
@Test
@Disabled
void textToSpeech() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.TTS_1_HD.getModel()).build(), OpenaiService.class);
final InputStream inputStream = openaiService.textToSpeech("万里山河一夜白,\n" +
"千峰尽染玉龙哀。\n" +
"长风卷起琼花碎,\n" +
"直上九霄揽月来。", OpenaiCommon.OpenaiSpeech.NOVA);
final String filePath = "your filePath";
final Path path = Paths.get(filePath);
try (final FileOutputStream outputStream = new FileOutputStream(filePath)) {
Files.createDirectories(path.getParent());
final byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
@Test
@Disabled
void speechToText() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.WHISPER_1.getModel()).build(), OpenaiService.class);
final File file = FileUtil.file("your filePath");
final String speechToText = openaiService.speechToText(file);
assertNotNull(speechToText);
}
@Test
@Disabled
void embeddingText() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.TEXT_EMBEDDING_3_SMALL.getModel()).build(), OpenaiService.class);
final String embeddingText = openaiService.embeddingText("萬里山河一夜白,千峰盡染玉龍哀,長風捲起瓊花碎,直上九霄闌月來");
assertNotNull(embeddingText);
}
@Test
@Disabled
void moderations() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.OMNI_MODERATION_LATEST.getModel()).build(), OpenaiService.class);
final String moderations = openaiService.moderations("你要玩游戏", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
assertNotNull(moderations);
}
@Test
@Disabled
void chatReasoning() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class);
final List<Message> messages = new ArrayList<>();
messages.add(new Message("system","你是现代抽象家"));
messages.add(new Message("user","给我一个KFC疯狂星期四的文案"));
final String chatReasoning = openaiService.chatReasoning(messages, OpenaiCommon.OpenaiReasoning.HIGH.getEffort());
assertNotNull(chatReasoning);
}
@Test
@Disabled
void chatReasoningStream() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class);
final List<Message> messages = new ArrayList<>();
messages.add(new Message("system","你是现代抽象家"));
messages.add(new Message("user","给我一个KFC疯狂星期四的文案"));
// 使用AtomicBoolean作为结束标志
AtomicBoolean isDone = new AtomicBoolean(false);
openaiService.chatReasoning(messages,OpenaiCommon.OpenaiReasoning.HIGH.getEffort(), data -> {
assertNotNull(data);
if (data.contains("[DONE]")) {
// 设置结束标志
isDone.set(true);
} else if (data.contains("\"error\"")) {
isDone.set(true);
}
});
// 轮询检查结束标志
while (!isDone.get()) {
ThreadUtil.sleep(100);
}
}
}

View File

@ -35,6 +35,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@ -101,7 +102,7 @@ class OpenaiServiceTest {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);
String prompt = "图片上有些什么?";
List<String> images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800");
List<String> images = Collections.singletonList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800");
// 使用AtomicBoolean作为结束标志
AtomicBoolean isDone = new AtomicBoolean(false);
@ -200,7 +201,7 @@ class OpenaiServiceTest {
void moderations() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.OMNI_MODERATION_LATEST.getModel()).build(), OpenaiService.class);
final String moderations = openaiService.moderations("你要杀人", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
final String moderations = openaiService.moderations("你要玩游戏", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
assertNotNull(moderations);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import java.lang.reflect.Method;
public class CacheableAnnotationAttribute implements AnnotationAttribute {
private volatile boolean valueInvoked;
private Object value;
private volatile Object value;
private boolean defaultValueInvoked;
private Object defaultValue;

View File

@ -11,6 +11,7 @@ import cn.hutool.core.util.PageUtil;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import java.util.function.Consumer;
/**
@ -721,4 +722,32 @@ public class ListUtil {
}
return list;
}
/**
* 将两个列表的元素按照索引一一配对通过指定的函数进行合并返回一个新的结果列表
* 新列表的长度将以两个输入列表中较短的那个为准
*
* @param <A> 第一个列表的元素类型
* @param <B> 第二个列表的元素类型
* @param <R> 结果列表的元素类型
* @param listA 第一个列表
* @param listB 第二个列表
* @param zipper 合并函数接收来自listA和listB的两个元素返回一个结果元素
* @return 合并后的新列表
* @since 5.8.42
*/
public static <A, B, R> List<R> zip(List<A> listA, List<B> listB, BiFunction<A, B, R> zipper) {
if (CollUtil.isEmpty(listA) || CollUtil.isEmpty(listB)) {
return new ArrayList<>();
}
Assert.notNull(zipper, "Zipper function must not be null");
final int size = Math.min(listA.size(), listB.size());
final List<R> result = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
result.add(zipper.apply(listA.get(i), listB.get(i)));
}
return result;
}
}

View File

@ -13,7 +13,7 @@ import java.util.Date;
* @author looly
* @since 5.8.13
*/
public class NumberWithFormat extends Number implements TypeConverter {
public class NumberWithFormat extends Number implements TypeConverter, Comparable<NumberWithFormat> {
private static final long serialVersionUID = 1L;
private final Number number;
@ -86,4 +86,13 @@ public class NumberWithFormat extends Number implements TypeConverter {
public String toString() {
return this.number.toString();
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public int compareTo(NumberWithFormat o) {
if(this.number instanceof Comparable && o.getNumber() instanceof Comparable) {
return ((Comparable) this.number).compareTo(o.getNumber());
}
return Double.compare(this.doubleValue(), o.doubleValue());
}
}

View File

@ -73,13 +73,13 @@ public class DateModifier {
case ROUND:
int min = isAM ? 0 : 12;
int max = isAM ? 11 : 23;
int href = (max - min) / 2 + 1;
int href = min + (max - min) / 2 + 1;
int value = calendar.get(Calendar.HOUR_OF_DAY);
calendar.set(Calendar.HOUR_OF_DAY, (value < href) ? min : max);
break;
}
// 处理下一级别字段
return modify(calendar, dateField + 1, modifyType);
return modify(calendar, dateField + 1, modifyType, truncateMillisecond);
}
final int endField = truncateMillisecond ? Calendar.SECOND : Calendar.MILLISECOND;

View File

@ -1901,6 +1901,8 @@ public class ImgUtil {
// issue#IAPZG7
// FileCacheImageOutputStream会产生临时文件此处关闭清除
IoUtil.close(output);
// issue#ID6VNJ
flush(image);
}
return true;
}

View File

@ -240,6 +240,7 @@ public class FileUtil extends PathUtil {
if (path == null) {
return new ArrayList<>(0);
}
path = getAbsolutePath(path);
int index = path.lastIndexOf(FileUtil.JAR_PATH_EXT);
if (index < 0) {
// 普通目录
@ -253,8 +254,6 @@ public class FileUtil extends PathUtil {
return paths;
}
// jar文件
path = getAbsolutePath(path);
// jar文件中的路径
index = index + FileUtil.JAR_FILE_EXT.length();
JarFile jarFile = null;

View File

@ -238,7 +238,7 @@ public class FileNameUtil {
// issue#I4W5FS@Gitee
final int secondToLastIndex = fileName.substring(0, index).lastIndexOf(StrUtil.DOT);
final String substr = fileName.substring(secondToLastIndex == -1 ? index : secondToLastIndex + 1);
if (StrUtil.containsAny(substr, SPECIAL_SUFFIX)) {
if (StrUtil.equalsAny(substr, SPECIAL_SUFFIX)) {
return substr;
}

View File

@ -9,6 +9,7 @@ import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.IdcardUtil;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -1123,8 +1124,11 @@ public class Validator {
Assert.notNull(value);
Assert.notNull(min);
Assert.notNull(max);
final double doubleValue = value.doubleValue();
return (doubleValue >= min.doubleValue()) && (doubleValue <= max.doubleValue());
// 通过 NumberUtil 转换为 BigDecimal使用 BigDecimal 进行比较以保留精度
BigDecimal valBd = NumberUtil.toBigDecimal(value);
BigDecimal minBd = NumberUtil.toBigDecimal(min);
BigDecimal maxBd = NumberUtil.toBigDecimal(max);
return valBd.compareTo(minBd) >= 0 && valBd.compareTo(maxBd) <= 0;
}
/**

View File

@ -168,7 +168,7 @@ public class MutableLong extends Number implements Comparable<MutableLong>, Muta
* </ol>
*
* @param obj 比对的对象
* @return 相同返回<code>true</code>否则 {@code false}
* @return 相同返回{@code true}否则 {@code false}
*/
@Override
public boolean equals(final Object obj) {

View File

@ -39,7 +39,7 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
}
this.inverse.put(value, key);
}
return super.put(key, value);
return oldValue;
}
@Override
@ -94,10 +94,12 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
@Override
public V putIfAbsent(K key, V value) {
if (null != this.inverse) {
this.inverse.putIfAbsent(value, key);
final V oldValue = super.putIfAbsent(key, value);
// 只有当oldValue为null时(即key之前不存在),才更新反向Map
if (null == oldValue && null != this.inverse) {
this.inverse.put(value, key);
}
return super.putIfAbsent(key, value);
return oldValue;
}
@Override

View File

@ -1,12 +1,9 @@
package cn.hutool.core.math;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.NumberUtil;
import java.io.Serializable;
import java.util.*;
/**
* 排列A(n, m)<br>
@ -47,10 +44,23 @@ public class Arrangement implements Serializable {
* @return 排列数
*/
public static long count(int n, int m) {
if (n == m) {
return NumberUtil.factorial(n);
if (m < 0 || m > n) {
throw new IllegalArgumentException("n >= 0 && m >= 0 && m <= n required");
}
return (n > m) ? NumberUtil.factorial(n, n - m) : 0;
if (m == 0) {
return 1;
}
long result = 1;
// n n-m+1 逐个乘
for (int i = 0; i < m; i++) {
long next = result * (n - i);
// 溢出检测
if (next < result) {
throw new ArithmeticException("Overflow computing A(" + n + "," + m + ")");
}
result = next;
}
return result;
}
/**
@ -77,26 +87,85 @@ public class Arrangement implements Serializable {
}
/**
* 排列选择从列表中选择m个排列
* 从当前数据中选择 m 个元素生成所有不重复的排列Permutation
*
* @param m 选择个数
* @return 所有排列列表
* <p>
* 说明
* <ul>
* <li>不允许重复选择同一个元素即经典排列 A(n, m)</li>
* <li>结果中不会出现 ["1","1"] 这种重复元素的情况</li>
* <li>顺序敏感因此 ["1","2"] ["2","1"] 都会包含</li>
* </ul>
*
* 数量公式
* <pre>
* A(n, m) = n! / (n - m)!
* </pre>
*
* 举例
* <pre>
* datas = ["1","2","3"]
* m = 2
* 输出
* ["1","2"]
* ["1","3"]
* ["2","1"]
* ["2","3"]
* ["3","1"]
* ["3","2"]
* 6 A(3,2)=6
* </pre>
*
* @param m 选择的元素个数
* @return 所有长度为 m 的不重复排列列表
*/
public List<String[]> select(int m) {
final List<String[]> result = new ArrayList<>((int) count(this.datas.length, m));
select(this.datas, new String[m], 0, result);
if (m < 0 || m > datas.length) {
return Collections.emptyList();
}
if (m == 0) {
// A(n,0) = 1唯一一个空排列
return Collections.singletonList(new String[0]);
}
long estimated = count(datas.length, m);
int capacity = estimated > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) estimated;
List<String[]> result = new ArrayList<>(capacity);
boolean[] visited = new boolean[datas.length];
dfs(new String[m], 0, visited, result);
return result;
}
/**
* 排列所有组合即A(n, 1) + A(n, 2) + A(n, 3)...
* 生成当前数据的全部不重复排列长度为 1 n 的所有排列
*
* @return 全排列结果
* <p>
* 说明
* <ul>
* <li>不允许重复选择元素 ["1","1"] ["2","2","3"] 这种</li>
* <li>包含所有长度 m=1..n 的排列</li>
* <li>总数量为 A(n,1) + A(n,2) + ... + A(n,n)</li>
* </ul>
*
* 举例datas = ["1","2","3"]
* <pre>
* m=1: ["1"], ["2"], ["3"] 3
* m=2: ["1","2"], ["1","3"], ["2","1"], ... 6
* m=3: ["1","2","3"], ["1","3","2"], ["2","1","3"], ... 6
*
* 总共3 + 6 + 6 = 15
* </pre>
*
* @return 所有不重复排列列表
*/
public List<String[]> selectAll() {
final List<String[]> result = new ArrayList<>((int) countAll(this.datas.length));
for (int i = 1; i <= this.datas.length; i++) {
result.addAll(select(i));
List<String[]> result = new ArrayList<>();
for (int m = 1; m <= datas.length; m++) {
result.addAll(select(m));
}
return result;
}
@ -124,4 +193,192 @@ public class Arrangement implements Serializable {
select(ArrayUtil.remove(datas, i), resultList, resultIndex + 1, result);
}
}
/**
* 返回一个排列的迭代器
*
* @param m 选择的元素个数
* @return 排列迭代器
*/
public Iterable<String[]> iterate(int m) {
return () -> new ArrangementIterator(datas, m);
}
/**
* 排列迭代器
*
* @author CherryRum
*/
private static class ArrangementIterator implements Iterator<String[]> {
private final String[] datas;
private final int n;
private final int m;
private final boolean[] visited;
private final String[] buffer;
// 每一层记录当前尝试的下标-1表示还未尝试
private final int[] indices;
private int depth;
private boolean end;
// 预取下一个元素
private String[] nextItem;
private boolean nextPrepared;
ArrangementIterator(String[] datas, int m) {
this.datas = datas;
this.n = datas.length;
this.m = m;
this.visited = new boolean[n];
this.nextItem = null;
this.nextPrepared = false;
if (m < 0 || m > n) {
// 无效或无解直接结束
this.indices = new int[Math.max(1, m)];
this.buffer = new String[Math.max(1, m)];
this.depth = -1;
this.end = true;
} else if (m == 0) {
// m == 0: 只返回一个空数组
this.indices = new int[0];
this.buffer = new String[0];
this.depth = 0;
this.end = false;
} else {
this.indices = new int[m];
Arrays.fill(this.indices, -1);
this.buffer = new String[m];
this.depth = 0;
this.end = false;
}
}
@Override
public boolean hasNext() {
if (end) return false;
if (nextPrepared) return nextItem != null;
prepareNext();
return nextItem != null;
}
@Override
public String[] next() {
if (end && !nextPrepared) {
throw new NoSuchElementException();
}
if (!nextPrepared) {
prepareNext();
}
if (nextItem == null) {
throw new NoSuchElementException();
}
String[] ret = nextItem;
// 清除预取缓存下一次需要重新准备
nextItem = null;
nextPrepared = false;
// 如果m == 0该项是唯一项迭代结束
if (m == 0) {
end = true;
}
return ret;
}
/**
* 将状态推进到下一个可返回的排列并把它放入 nextItem
* 如果无更多排列则将 end=true 并把 nextItem 置为 null
*/
private void prepareNext() {
// 已经准备过或已结束
if (nextPrepared || end) {
nextPrepared = true;
return;
}
// special-case m == 0
if (m == 0) {
nextItem = new String[0];
nextPrepared = true;
// do not set end here; end will be set after returning this element in next()
return;
}
// 非递归模拟DFS直到找到一个可返回的排列或穷尽
while (depth >= 0) {
int start = indices[depth] + 1;
boolean found = false;
for (int i = start; i < n; i++) {
if (!visited[i]) {
// 如果当前层之前有选过一个元素要先取消之前选中的 visited
if (indices[depth] != -1) {
visited[indices[depth]] = false;
}
indices[depth] = i;
visited[i] = true;
buffer[depth] = datas[i];
found = true;
break;
}
}
if (!found) {
// 本层没有可用元素回溯
if (indices[depth] != -1) {
visited[indices[depth]] = false;
indices[depth] = -1;
}
depth--;
continue;
}
// 若已达到输出深度准备输出但不抛出
if (depth == m - 1) {
nextItem = Arrays.copyOf(buffer, m);
// 取消当前visited为下一次在同一层寻找下一个候选做准备
visited[indices[depth]] = false;
// 保持 depth 不变下一次 prepare 会从 indices[depth]+1 开始寻找
nextPrepared = true;
return;
} else {
// 向下一层深入初始化下一层为-1并继续循环
depth++;
if (depth < m) {
indices[depth] = -1;
}
}
}
// 若循环结束说明已经穷尽所有可能
end = true;
nextItem = null;
nextPrepared = true;
}
}
/**
* 核心递归方法回溯算法
* * @param current 当前构建的排列数组
*
* @param depth 当前递归深度填到了第几个位置
* @param visited 标记数组记录哪些索引已经被使用了
* @param result 结果集
*/
private void dfs(String[] current, int depth, boolean[] visited, List<String[]> result) {
if (depth == current.length) {
result.add(Arrays.copyOf(current, current.length));
return;
}
for (int i = 0; i < datas.length; i++) {
if (!visited[i]) {
visited[i] = true;
current[depth] = datas[i];
dfs(current, depth + 1, visited, result);
visited[i] = false;
}
}
}
}

View File

@ -1,13 +1,13 @@
package cn.hutool.core.math;
import cn.hutool.core.util.StrUtil;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
/**
* 组合即C(n, m)<br>
* 排列组合相关类 参考http://cgs1999.iteye.com/blog/2327664
@ -32,17 +32,71 @@ public class Combination implements Serializable {
/**
* 计算组合数即C(n, m) = n!/((n-m)! * m!)
*
* <p>注意此方法内部使用 BigInteger 修复了旧版 factorial 的计算错误
* 但最终仍以 long 返回因此当结果超过 long 范围时仍会溢出</p>
* <p>建议使用 {@link #countBig(int, int)} 获取精确结果或使用
* {@link #countSafe(int, int)} 获取安全 long 版本</p>
* @param n 总数
* @param m 选择的个数
* @return 组合数
*/
@Deprecated
public static long count(int n, int m) {
if (0 == m || n == m) {
return 1;
BigInteger big = countBig(n, m);
return big.longValue();
}
return (n > m) ? NumberUtil.factorial(n, n - m) / NumberUtil.factorial(m) : 0;
/**
* 计算组合数 C(n, m) BigInteger 精确版本
* 使用逐步累乘除法非阶乘保证不溢出性能好
* <p>
* 数学定义
* C(n, m) = n! / (m! (n - m)!)
* <p>
* 优化方式
* 1. 利用对称性 m = min(m, n-m)
* 2. 每一步先乘 BigInteger再除以当前 i保证数值不暴涨
*
* @param n 总数 n必须 大于等于 0
* @param m 取出 m必须 大于等于 0
* @return C(n, m) BigInteger 精确值 m 大于 n 时返回 BigInteger.ZERO
*/
public static BigInteger countBig(int n, int m) {
if (n < 0 || m < 0) {
throw new IllegalArgumentException("n and m must be non-negative. got n=" + n + ", m=" + m);
}
if (m > n) {
return BigInteger.ZERO;
}
if (m == 0 || n == m) {
return BigInteger.ONE;
}
// 使用对称性C(n, m) = C(n, n-m)
m = Math.min(m, n - m);
BigInteger result = BigInteger.ONE;
// 1 m 累乘
for (int i = 1; i <= m; i++) {
int numerator = n - m + i;
result = result.multiply(BigInteger.valueOf(numerator))
.divide(BigInteger.valueOf(i));
}
return result;
}
/**
* 安全组合数 long 版本
*
* @param n 总数 n必须 大于等于 0
* @param m 取出 m必须 大于等于 0
* <p>若结果超出 long 范围会抛 ArithmeticException而非溢出</p>
* @return C(n, m) long 精确值 m 大于 n 时返回 0L
*/
public static long countSafe(int n, int m) {
BigInteger big = countBig(n, m);
return big.longValueExact();
}
/**
* 计算组合总数即C(n, 1) + C(n, 2) + C(n, 3)...
@ -104,4 +158,5 @@ public class Combination implements Serializable {
select(i + 1, resultList, resultIndex + 1, result);
}
}
}

View File

@ -4,14 +4,34 @@ import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 本地端口生成器<br>
* 用于生成本地可用未被占用的端口号<br>
* 注意多线程甚至单线程访问时可能会返回同一端口例如获取了端口但是没有使用
* 本地端口生成器LocalPortGenerator
* <p>
* 当前类名中Generater为拼写错误正确应为 Generator为保持兼容性暂未更改
* 该问题将在后续大版本中以重命名方式修复并保留旧类名的弃用@Deprecated兼容层
* <p>
*
* 用于从指定起点开始递增探测一个当前可用的本地端口探测通过短暂绑定
* {@link java.net.ServerSocket}以及可选 UDP DatagramSocket完成但不会真正占用端口
* <p>注意</p>
* <ul>
* <li>该方法执行的是端口探测分配返回端口不保证实际使用时仍然可用</li>
* <li>存在 TOCTOU检测到使用之间竞态多线程下可能返回同一端口</li>
* <li>UDP 探测可能导致误判TCP 可用但 UDP 被占用</li>
* <li>不适合作为生产级端口分配策略推荐使用 {@code new ServerSocket(0)}</li>
* </ul>
*
* <p>未来版本计划</p>
* <ul>
* <li>修复类名拼写问题Generater更名为 Generator</li>
* <li>提供真正可靠的端口获取实现绑定即占用避免竞态</li>
* <li>优化探测策略减少不必要的 UDP 检测</li>
* <li>提供更安全的随机端口生成 API</li>
* </ul>
* @author looly
* @since 4.0.3
*
*/
public class LocalPortGenerater implements Serializable{
private static final long serialVersionUID = 1L;

View File

@ -28,6 +28,9 @@ import java.util.function.Predicate;
*/
public class CharSequenceUtil {
/**
* 索引值{@code -1}
*/
public static final int INDEX_NOT_FOUND = Finder.INDEX_NOT_FOUND;
/**
@ -1557,7 +1560,7 @@ public class CharSequenceUtil {
final String str2 = str.toString();
int toIndex = str2.length();
while (str2.startsWith(suffixStr, toIndex - suffixLength)){
while (str2.startsWith(suffixStr, toIndex - suffixLength)) {
toIndex -= suffixLength;
}
return subPre(str2, toIndex);
@ -1707,17 +1710,17 @@ public class CharSequenceUtil {
if (startWith(str2, prefix, ignoreCase)) {
from = prefix.length();
if(from == to){
if (from == to) {
// "a", "a", "a" -> ""
return EMPTY;
}
}
if (endWith(str2, suffix, ignoreCase)) {
to -= suffix.length();
if(from == to){
if (from == to) {
// "a", "a", "a" -> ""
return EMPTY;
} else if(to < from){
} else if (to < from) {
// pre去除后和suffix有重叠 ("aba", "ab", "ba") -> "a"
to += suffix.length();
}
@ -1787,22 +1790,22 @@ public class CharSequenceUtil {
int from = 0;
int to = str2.length();
if(!prefixStr.isEmpty()){
if (!prefixStr.isEmpty()) {
while (str2.startsWith(prefixStr, from)) {
from += prefix.length();
if(from == to){
if (from == to) {
// "a", "a", "a" -> ""
return EMPTY;
}
}
}
if(!suffixStr.isEmpty()){
if (!suffixStr.isEmpty()) {
while (str2.startsWith(suffixStr, to - suffixStr.length())) {
to -= suffixStr.length();
if(from == to){
if (from == to) {
// "a", "a", "a" -> ""
return EMPTY;
}else if(to < from){
} else if (to < from) {
// pre去除后和suffix有重叠 ("aba", "ab", "ba") -> "a"
to += suffixStr.length();
break;
@ -4263,7 +4266,7 @@ public class CharSequenceUtil {
if (null == str) {
return null;
}
if(0 == str.length()){
if (0 == str.length()) {
return EMPTY;
}
return str.toString().toLowerCase();
@ -4281,7 +4284,7 @@ public class CharSequenceUtil {
if (null == str) {
return null;
}
if(0 == str.length()){
if (0 == str.length()) {
return EMPTY;
}
return str.toString().toUpperCase();
@ -4559,9 +4562,21 @@ public class CharSequenceUtil {
* @return StringBuilder对象
*/
public static StringBuilder builder(CharSequence... strs) {
return builder(Function.identity(), strs);
}
/**
* 创建StringBuilder对象
*
* @param strEditor 编辑器用于对每个字符串进行编辑
* @param strs 待处理的字符串列表
* @return StringBuilder对象
* @since 5.8.42
*/
public static StringBuilder builder(Function<CharSequence, CharSequence> strEditor, final CharSequence... strs) {
final StringBuilder sb = new StringBuilder();
for (CharSequence str : strs) {
sb.append(str);
for (final CharSequence str : strs) {
sb.append(strEditor.apply(str));
}
return sb;
}

View File

@ -124,14 +124,16 @@ public class PasswdStrength {
}
}
// decrease points
if ("abcdefghijklmnopqrstuvwxyz".indexOf(passwd) > 0 || "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(passwd) > 0) {
// 判断passwd是否为连续字母a-z/A-Z的完整子串
if ("abcdefghijklmnopqrstuvwxyz".contains(passwd) || "ABCDEFGHIJKLMNOPQRSTUVWXYZ".contains(passwd)) {
level--;
}
if ("qwertyuiop".indexOf(passwd) > 0 || "asdfghjkl".indexOf(passwd) > 0 || "zxcvbnm".indexOf(passwd) > 0) {
// 判断passwd是否为键盘连续序列的完整子串
if ("qwertyuiop".contains(passwd) || "asdfghjkl".contains(passwd) || "zxcvbnm".contains(passwd)) {
level--;
}
if (StrUtil.isNumeric(passwd) && ("01234567890".indexOf(passwd) > 0 || "09876543210".indexOf(passwd) > 0)) {
// 判断passwd是否为纯数字弱密码升序或降序的完整子串
if (StrUtil.isNumeric(passwd) && ("01234567890".contains(passwd) || "09876543210".contains(passwd))) {
level--;
}
@ -172,6 +174,7 @@ public class PasswdStrength {
}
}
// 检测密码是否为简单密码字典中的弱密码或包含字典弱密码片段
for (String s : DICTIONARY) {
if (passwd.equals(s) || s.contains(passwd)) {
level--;
@ -201,7 +204,7 @@ public class PasswdStrength {
}
/**
* Get password strength level, includes easy, midium, strong, very strong, extremely strong
* 获取密码强度等级, 包括 easy, medium, strong, very strong, extremely strong
*
* @param passwd 密码
* @return 密码等级枚举
@ -232,8 +235,7 @@ public class PasswdStrength {
}
/**
* Check character's type, includes num, capital letter, small letter and other character.
* 检查字符类型
* 检查字符类型包括数字大写字母小写字母及其他字符
*
* @param c 字符
* @return 类型

View File

@ -114,7 +114,7 @@ public final class CsvParser extends ComputeIter<CsvRow> implements Closeable, S
/**
* 读取下一行数据
*
* @return CsvRow{@code null}表示
* @return CsvRow{@code null}表示读取结束
* @throws IORuntimeException IO读取异常
*/
public CsvRow nextRow() throws IORuntimeException {

View File

@ -71,13 +71,16 @@ public class SplitIter extends ComputeIter<String> implements Serializable {
return text.substring(offset);
}
final int start = finder.start(offset);
String result = null;
int start;
do {
start = finder.start(offset);
// 无分隔符结束
if (start < 0) {
// 如果不再有分隔符但是遗留了字符则单独作为一个段
if (offset <= text.length()) {
final String result = text.substring(offset);
if (false == ignoreEmpty || false == result.isEmpty()) {
result = text.substring(offset);
if (!ignoreEmpty || !result.isEmpty()) {
// 返回非空串
offset = Integer.MAX_VALUE;
return result;
@ -87,13 +90,9 @@ public class SplitIter extends ComputeIter<String> implements Serializable {
}
// 找到新的分隔符位置
final String result = text.substring(offset, start);
result = text.substring(offset, start);
offset = finder.end(start);
if (ignoreEmpty && result.isEmpty()) {
// 发现空串且需要忽略时跳过之
return computeNext();
}
} while (ignoreEmpty && result.isEmpty()); // 空串则继续循环
count++;
return result;

View File

@ -1082,7 +1082,7 @@ public class ClassUtil {
/**
* 获取class类路径URL, 不管是否在jar包中都会返回文件夹的路径<br>
* class在jar包中返回jar所在文件夹,class不在jar中返回文件夹目录<br>
* class在jar包中返回jar的路径,class不在jar中返回文件夹目录<br>
* jdk中的类不能使用此方法
*
* @param clazz
@ -1098,7 +1098,7 @@ public class ClassUtil {
/**
* 获取class类路径, 不管是否在jar包中都会返回文件夹的路径<br>
* class在jar包中返回jar所在文件夹,class不在jar中返回文件夹目录<br>
* class在jar包中返回jar的路径,class不在jar中返回文件夹目录<br>
* jdk中的类不能使用此方法
*
* @param clazz

View File

@ -100,7 +100,7 @@ public class CreditCodeUtil {
//
for (int i = 0; i < 2; i++) {
int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length - 1);
int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length);
buf.append(Character.toUpperCase(BASE_CODE_ARRAY[num]));
}
for (int i = 2; i < 8; i++) {
@ -108,7 +108,7 @@ public class CreditCodeUtil {
buf.append(BASE_CODE_ARRAY[num]);
}
for (int i = 8; i < 17; i++) {
int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length - 1);
int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length);
buf.append(BASE_CODE_ARRAY[num]);
}

View File

@ -10,7 +10,7 @@ import cn.hutool.core.text.escape.XmlUnescape;
* 转义和反转义工具类Escape / Unescape<br>
* escape采用ISO Latin字符集对指定的字符串进行编码<br>
* 所有的空格符标点符号特殊字符以及其他非ASCII字符都将被转化成%xx格式的字符编码(xx等于该字符在字符集表里面的编码的16进制数字)
* TODO 6.x迁移到core.text.escape包下
* TODO 7.x迁移到core.text.escape包下
*
* @author xiaoleilu
*/
@ -20,7 +20,7 @@ public class EscapeUtil {
* 不转义的符号编码
*/
private static final String NOT_ESCAPE_CHARS = "*@-_+./";
private static final Filter<Character> JS_ESCAPE_FILTER = c -> false == (
private static final Filter<Character> JS_ESCAPE_FILTER = c -> !(
Character.isDigit(c)
|| Character.isLowerCase(c)
|| Character.isUpperCase(c)
@ -122,7 +122,7 @@ public class EscapeUtil {
char c;
for (int i = 0; i < content.length(); i++) {
c = content.charAt(i);
if (false == filter.accept(c)) {
if (!filter.accept(c)) {
tmp.append(c);
} else if (c < 256) {
tmp.append("%");
@ -143,36 +143,69 @@ public class EscapeUtil {
}
/**
* Escape解码
* Escape解码支持两种转义格式的解码
* <ul>
* <li>%XX - 两位十六进制数字用于表示ASCII字符0-255</li>
* <li>%uXXXX - 四位十六进制数字用于表示Unicode字符</li>
* </ul>
* <p>
* 对于不完整的转义序列本方法会将其原样保留而不抛出异常
* <ul>
* <li>字符串末尾的单独"%"字符会被原样保留</li>
* <li>"%u"后面不足4位十六进制数字时整个不完整序列会被原样保留</li>
* <li>"%"后面不足2位十六进制数字时%u格式整个不完整序列会被原样保留</li>
* </ul>
* 例如
* <pre>
* unescape("test%") = "test%" // 末尾的%被保留
* unescape("test%u12") = "test%u12" // 不足4位原样保留
* unescape("test%2") = "test%2" // 不足2位原样保留
* unescape("test%20") = "test " // 正常解码空格
* unescape("test%u4E2D") = "test中" // 正常解码中文字符
* </pre>
*
* @param content 被转义的内容
* @return 解码后的字符串
*/
public static String unescape(String content) {
public static String unescape(final String content) {
if (StrUtil.isBlank(content)) {
return content;
}
StringBuilder tmp = new StringBuilder(content.length());
final int len = content.length();
final StringBuilder tmp = new StringBuilder(len);
int lastPos = 0;
int pos;
char ch;
while (lastPos < content.length()) {
while (lastPos < len) {
pos = content.indexOf("%", lastPos);
if (pos == lastPos) {
if (content.charAt(pos + 1) == 'u') {
if (pos + 1 < len && content.charAt(pos + 1) == 'u') {
if (pos + 6 <= len) {
ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
tmp.append(ch);
lastPos = pos + 6;
} else {
// Not enough characters, append as-is
tmp.append(content.substring(pos));
lastPos = len;
}
} else {
// Check if there's enough characters for hex escape (%XX)
if (pos + 3 <= len) {
ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
tmp.append(ch);
lastPos = pos + 3;
} else {
// Not enough characters, append as-is
tmp.append(content.substring(pos));
lastPos = len;
}
}
} else {
if (pos == -1) {
tmp.append(content.substring(lastPos));
lastPos = content.length();
lastPos = len;
} else {
tmp.append(content, lastPos, pos);
lastPos = pos;

View File

@ -27,7 +27,7 @@ public class HexUtil {
* @return 是否为16进制
*/
public static boolean isHexNumber(String value) {
if(StrUtil.startWith(value, '-')){
if (StrUtil.startWith(value, '-')) {
// issue#2875
return false;
}
@ -35,7 +35,7 @@ public class HexUtil {
if (value.startsWith("0x", index) || value.startsWith("0X", index)) {
index += 2;
} else if (value.startsWith("#", index)) {
index ++;
index++;
}
try {
new BigInteger(value.substring(index), 16);
@ -304,7 +304,7 @@ public class HexUtil {
* @since 5.7.4
*/
public static int hexToInt(String value) {
return Integer.parseInt(value, 16);
return Integer.parseInt(removeHexPrefix(value), 16);
}
/**
@ -326,7 +326,7 @@ public class HexUtil {
* @since 5.7.4
*/
public static long hexToLong(String value) {
return Long.parseLong(value, 16);
return Long.parseLong(removeHexPrefix(value), 16);
}
/**
@ -352,7 +352,7 @@ public class HexUtil {
if (null == hexStr) {
return null;
}
return new BigInteger(hexStr, 16);
return new BigInteger(removeHexPrefix(hexStr), 16);
}
/**
@ -379,17 +379,46 @@ public class HexUtil {
* @return 格式化后的字符串
*/
public static String format(final String hexStr, String prefix) {
if (StrUtil.isEmpty(hexStr)) {
return StrUtil.EMPTY;
}
if (null == prefix) {
prefix = StrUtil.EMPTY;
}
final int length = hexStr.length();
final StringBuilder builder = StrUtil.builder(length + length / 2 + (length / 2 * prefix.length()));
builder.append(prefix).append(hexStr.charAt(0)).append(hexStr.charAt(1));
for (int i = 2; i < length - 1; i += 2) {
builder.append(CharUtil.SPACE).append(prefix).append(hexStr.charAt(i)).append(hexStr.charAt(i + 1));
for (int i = 0; i < length; i++) {
if (i % 2 == 0) {
if (i != 0) {
builder.append(CharUtil.SPACE);
}
builder.append(prefix);
}
builder.append(hexStr.charAt(i));
}
return builder.toString();
}
/**
* 去除十六进制字符串的常见前缀 0x0X#
*
* @param hexStr 十六进制字符串
* @return 去除前缀后的字符串
*/
private static String removeHexPrefix(String hexStr) {
if (StrUtil.length(hexStr) > 1) {
final char c0 = hexStr.charAt(0);
switch (c0) {
case '0':
if (hexStr.charAt(1) == 'x' || hexStr.charAt(1) == 'X') {
return hexStr.substring(2);
}
case '#':
return hexStr.substring(1);
}
}
return hexStr;
}
}

View File

@ -72,14 +72,14 @@ public class ObjectUtil {
}
/**
* 计算对象长度如果是字符串调用其length函数集合类调用其size函数数组调用其length属性其他可遍历对象遍历计算长度<br>
* 支持的类型包括
* <p>计算对象长度支持类型包括
* <ul>
* <li>CharSequence</li>
* <li>Map</li>
* <li>Iterator</li>
* <li>Enumeration</li>
* <li>Array</li>
* <li>{@code null}默认返回{@code 0}</li>
* <li>数组返回数组长度</li>
* <li>{@link CharSequence}返回{@link CharSequence#length()}</li>
* <li>{@link Collection}返回{@link Collection#size()}</li>
* <li>{@link Iterator}{@link Iterable}可迭代的元素数量副作用{@link Iterator}只能被迭代一次</li>
* <li>{@link Enumeration}返回可迭代的元素数量副作用{@link Enumeration}只能被迭代一次</li>
* </ul>
*
* @param obj 被计算长度的对象
@ -128,7 +128,7 @@ public class ObjectUtil {
* 对象中是否包含元素<br>
* 支持的对象类型包括
* <ul>
* <li>String</li>
* <li>CharSequence</li>
* <li>Collection</li>
* <li>Map</li>
* <li>Iterator</li>
@ -144,11 +144,22 @@ public class ObjectUtil {
if (obj == null) {
return false;
}
if (obj instanceof String) {
if (element == null) {
if (obj instanceof CharSequence) {
if (!(element instanceof CharSequence)) {
return false;
}
return ((String) obj).contains(element.toString());
String elementStr;
try {
elementStr = element.toString();
// 检查 toString() 返回 null 的情况
if (elementStr == null) {
return false;
}
} catch (Exception e) {
// 如果toString抛异常认为不包含
return false;
}
return obj.toString().contains(elementStr);
}
if (obj instanceof Collection) {
return ((Collection<?>) obj).contains(element);

View File

@ -888,6 +888,7 @@ public class ReflectUtil {
return (T) ClassUtil.getPrimitiveDefaultValue(type);
}
if (Object.class != type) {
// 某些特殊接口的实例化按照默认实现进行
if (type.isAssignableFrom(AbstractMap.class)) {
type = (Class<T>) HashMap.class;
@ -895,6 +896,11 @@ public class ReflectUtil {
type = (Class<T>) ArrayList.class;
} else if (type.isAssignableFrom(Set.class)) {
type = (Class<T>) HashSet.class;
} else if (type.isAssignableFrom(Queue.class)) {
type = (Class<T>) LinkedList.class;
} else if (type.isAssignableFrom(Deque.class)) {
type = (Class<T>) LinkedList.class;
}
}
try {

View File

@ -187,7 +187,7 @@ public class StrUtil extends CharSequenceUtil implements StrPool {
/**
* 解码字节码
*
* @param data 字符串
* @param data byte数组
* @param charset 字符集如果此字段为空则解码的结果取决于平台
* @return 解码后的字符串
*/
@ -216,7 +216,7 @@ public class StrUtil extends CharSequenceUtil implements StrPool {
/**
* 解码字节码
*
* @param data 字符串
* @param data Byte数组
* @param charset 字符集如果此字段为空则解码的结果取决于平台
* @return 解码后的字符串
*/
@ -261,7 +261,7 @@ public class StrUtil extends CharSequenceUtil implements StrPool {
if (null == charset) {
charset = Charset.defaultCharset();
}
return charset.decode(data).toString();
return charset.decode(data.duplicate()).toString();
}
/**

View File

@ -34,6 +34,12 @@ public class TypeUtil {
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
return (Class<?>) ((ParameterizedType) type).getRawType();
} else if (type instanceof GenericArrayType) {
final Type componentType = ((GenericArrayType) type).getGenericComponentType();
final Class<?> componentClass = getClass(componentType);
if (componentClass != null) {
return Array.newInstance(componentClass, 0).getClass();
}
} else if (type instanceof TypeVariable) {
Type[] bounds = ((TypeVariable<?>) type).getBounds();
if (bounds.length == 1) {

View File

@ -270,7 +270,7 @@ public class URLUtil extends URLEncodeUtil {
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw new UtilException(e, "Error occured when get URL!");
throw new UtilException(e, "Error occurred when get URL!");
}
}
@ -288,7 +288,7 @@ public class URLUtil extends URLEncodeUtil {
urls[i] = files[i].toURI().toURL();
}
} catch (MalformedURLException e) {
throw new UtilException(e, "Error occured when get URL!");
throw new UtilException(e, "Error occurred when get URL!");
}
return urls;
@ -807,8 +807,9 @@ public class URLUtil extends URLEncodeUtil {
} else {
// 如果资源打在jar包中或来自网络使用网络请求长度
// issue#3226, 来自Spring的AbstractFileResolvingResource
URLConnection con = null;
try {
final URLConnection con = url.openConnection();
con = url.openConnection();
useCachesIfNecessary(con);
if (con instanceof HttpURLConnection) {
final HttpURLConnection httpCon = (HttpURLConnection) con;
@ -817,6 +818,10 @@ public class URLUtil extends URLEncodeUtil {
return con.getContentLengthLong();
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
if (con instanceof HttpURLConnection) {
((HttpURLConnection) con).disconnect();
}
}
}
}

View File

@ -54,7 +54,7 @@ public class VersionUtil {
/**
* 当前版本大于待比较版本
*
* @param currentVersion 当前
* @param currentVersion 当前
* @param compareVersion 待比较版本
* @return true 当前版本大于待比较版本
*/
@ -65,7 +65,7 @@ public class VersionUtil {
/**
* 当前版本大于等于待比较版本
*
* @param currentVersion 当前
* @param currentVersion 当前
* @param compareVersion 待比较版本
* @return true 当前版本大于等于待比较版本
*/
@ -76,7 +76,7 @@ public class VersionUtil {
/**
* 当前版本小于待比较版本
*
* @param currentVersion 当前
* @param currentVersion 当前
* @param compareVersion 待比较版本
* @return true 当前版本小于待比较版本
*/
@ -87,7 +87,7 @@ public class VersionUtil {
/**
* 当前版本小于等于待比较版本
*
* @param currentVersion 当前
* @param currentVersion 当前
* @param compareVersion 待比较版本
* @return true 当前版本小于等于待比较版本
*/
@ -105,9 +105,9 @@ public class VersionUtil {
* matchEl("1.0.2", "1.0.0-1.1.1") == true
* }</pre>
*
* @param currentVersion 当前
* @param currentVersion 当前
* @param versionEl 版本表达式
* @return true 当前版本小于等于待比较版本
* @return true 当前版本是否满足版本表达式
*/
public static boolean matchEl(String currentVersion, String versionEl) {
return matchEl(currentVersion, versionEl, defaultVersionsDelimiter);
@ -123,7 +123,7 @@ public class VersionUtil {
* matchEl("1.0.2", "1.0.1,1.0.2-1.1.1", ",") == true
* }</pre>
*
* @param currentVersion 当前
* @param currentVersion 当前
* @param versionEl 版本表达式可以匹配多个条件使用指定的分隔符默认;分隔,
* {@code '-'}表示范围包含左右版本,如果 {@code '-'}的左边没有表示小于等于某个版本号 右边表示大于等于某个版本号
* 支持比较符号{@code '>'},{@code '<'}, {@code '>='},{@code '<='}{@code '≤'}{@code '≥'}
@ -133,7 +133,7 @@ public class VersionUtil {
* <li>{@code >=2.0.0, 1.9.8} 表示版本号 大于等于{@code 2.0.0} 版本{@code 1.9.8}</li>
* </ul>
* @param versionsDelimiter 多表达式分隔符
* @return true 当前版本小于等于待比较版本
* @return true 当前版本是否满足版本表达式
*/
public static boolean matchEl(String currentVersion, String versionEl, String versionsDelimiter) {
if (StrUtil.isBlank(versionsDelimiter)
@ -185,9 +185,9 @@ public class VersionUtil {
return false;
}
} else if (StrUtil.contains(el, "-")) {
String[] pair = el.split("-");
String left = StrUtil.blankToDefault(StrUtil.trim(pair[0]), "");
String right = StrUtil.blankToDefault(StrUtil.trim(pair[1]), "");
int index = el.indexOf('-');
String left = StrUtil.blankToDefault(StrUtil.trim(el.substring(0, index)), "");
String right = StrUtil.blankToDefault(StrUtil.trim(el.substring(index + 1)), "");
boolean leftMatch = StrUtil.isBlank(left) || StrUtil.compareVersion(left, trimmedVersion) <= 0;
boolean rightMatch = StrUtil.isBlank(right) || StrUtil.compareVersion(right, trimmedVersion) >= 0;

View File

@ -1516,4 +1516,20 @@ public class CollUtilTest {
assertTrue(CollUtil.containsAll(coll1, coll2));
}
@Test
void finOneTest(){
Animal dog = new Animal("dog", 2);
Animal cat = new Animal("cat", 3);
Animal bear = new Animal("bear", 4);
List<Animal> list = new ArrayList<>();
list.add(dog);
list.add(cat);
list.add(bear);
final Animal cat1 = CollUtil.findOne(list, (t) -> t.getName().equals("cat"));
assertNotNull(cat1);
assertEquals("cat", cat1.getName());
}
}

View File

@ -130,6 +130,33 @@ public class DateUtilTest {
assertEquals("2020-02-29 12:59:59.000", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));
}
@Test
public void cellingAmPmTest(){
final String dateStr2 = "2020-02-29 10:59:34";
final Date date2 = DateUtil.parse(dateStr2);
DateTime dateTime = DateUtil.ceiling(date2, DateField.AM_PM);
assertEquals("2020-02-29 11:59:59.999", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));
dateTime = DateUtil.ceiling(date2, DateField.AM_PM, true);
assertEquals("2020-02-29 11:59:59.000", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));
}
@Test void roundAmPmTest() {
final String dateStr = "2020-02-29 13:59:34";
final Date date = DateUtil.parse(dateStr);
DateTime dateTime = DateUtil.round(date, DateField.AM_PM);
assertEquals("2020-02-29 12:59:59.000", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));
final String dateStr2 = "2020-02-29 18:59:34";
final Date date2 = DateUtil.parse(dateStr2);
DateTime dateTime2 = DateUtil.round(date2, DateField.AM_PM);
assertEquals("2020-02-29 23:59:59.000", dateTime2.toString(DatePattern.NORM_DATETIME_MS_PATTERN));
}
@Test
public void ceilingDayTest() {
final String dateStr2 = "2020-02-29 12:59:34";

View File

@ -1,6 +1,7 @@
package cn.hutool.core.io;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
@ -63,4 +64,21 @@ public class BufferUtilTest {
// 读取剩余部分
assertEquals("cc", StrUtil.utf8Str(BufferUtil.readBytes(buffer)));
}
@Test
public void testByteBufferSideEffect() {
String originalText = "Hello";
ByteBuffer buffer = ByteBuffer.wrap(originalText.getBytes(StandardCharsets.UTF_8));
// 此时 buffer.remaining() == 5
assertEquals(5, buffer.remaining());
// 调用工具类转换打印buffer内容
String result = StrUtil.str(buffer, StandardCharsets.UTF_8);
assertEquals(originalText, result);
// 预期
// 工具类不应该修改原 buffer 的指针remaining 应该依然为 5
// 再次调用工具类转换输出结果应该不变
assertEquals(originalText, StrUtil.str(buffer, StandardCharsets.UTF_8));
}
}

View File

@ -19,4 +19,12 @@ public class FileNameUtilTest {
final String s = FileNameUtil.mainName("abc.tar.gz");
assertEquals("abc", s);
}
@Test
public void extNameAndMainNameBugTest() {
// 正确输出前缀为 "app-v2.3.1-star"
assertEquals("app-v2.3.1-star",FileNameUtil.mainName("app-v2.3.1-star.gz"));
// 当前代码会失败预期后缀结果 "gz"但是输出 "star.gz"
assertEquals("gz", FileNameUtil.extName("app-v2.3.1-star.gz"));
}
}

View File

@ -6,8 +6,9 @@ import cn.hutool.core.thread.ThreadUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class SimpleCacheTest {
@ -61,4 +62,36 @@ public class SimpleCacheTest {
assertEquals("aaaValue", cache.get("aaa"));
IoUtil.close(tester);
}
@Test
void removeTest(){
final SimpleCache<String, String> cache = new SimpleCache<>();
cache.put("key1", "value1");
cache.get("key1");
cache.put("key2", "value2");
cache.get("key2");
cache.put("key3", "value3");
cache.get("key3");
cache.put("key4", "value4");
cache.get("key4");
cache.get("key5", ()->"value5");
String key = null;
for (Map.Entry<String, String> entry : cache) {
if ("value3".equals(entry.getValue())) {
key = entry.getKey();
break;
}
}
if(null != key){
cache.remove(key);
}
assertEquals("value1", cache.get("key1"));
assertEquals("value2", cache.get("key2"));
assertEquals("value4", cache.get("key4"));
assertEquals("value5", cache.get("key5"));
assertNull(cache.get("key3"));
}
}

View File

@ -227,6 +227,19 @@ public class ValidatorTest {
assertTrue(Validator.isBetween(0.19, 0.1, 0.2));
}
@Test
public void isBetweenPrecisionLossTest() {
// 使用超过 double 精度的值
long base = 10000000000000000L;
long min = base + 1;
long max = base + 2;
// double 转换下basemin max 是完全相等的因为 double 精度不够
// 预期结果为false但是因为double 精度不够导致输出为true
assertFalse(Validator.isBetween(base, min, max));
}
@Test
public void isCarVinTest() {
assertTrue(Validator.isCarVin("LSJA24U62JG269225"));

View File

@ -1,20 +1,23 @@
package cn.hutool.core.math;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import cn.hutool.core.lang.Console;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import cn.hutool.core.lang.Console;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* 排列单元测试
* @author looly
*
*/
public class ArrangementTest {
// ----------------------------------------------------
// 基础测试
// ----------------------------------------------------
@Test
public void arrangementTest() {
long result = Arrangement.count(4, 2);
@ -30,37 +33,164 @@ public class ArrangementTest {
assertEquals(64, resultAll);
}
// ----------------------------------------------------
// select 基础测试
// ----------------------------------------------------
@Test
public void selectTest() {
Arrangement arrangement = new Arrangement(new String[] { "1", "2", "3", "4" });
Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3", "4"});
List<String[]> list = arrangement.select(2);
assertEquals(Arrangement.count(4, 2), list.size());
assertArrayEquals(new String[] {"1", "2"}, list.get(0));
assertArrayEquals(new String[] {"1", "3"}, list.get(1));
assertArrayEquals(new String[] {"1", "4"}, list.get(2));
assertArrayEquals(new String[] {"2", "1"}, list.get(3));
assertArrayEquals(new String[] {"2", "3"}, list.get(4));
assertArrayEquals(new String[] {"2", "4"}, list.get(5));
assertArrayEquals(new String[] {"3", "1"}, list.get(6));
assertArrayEquals(new String[] {"3", "2"}, list.get(7));
assertArrayEquals(new String[] {"3", "4"}, list.get(8));
assertArrayEquals(new String[] {"4", "1"}, list.get(9));
assertArrayEquals(new String[] {"4", "2"}, list.get(10));
assertArrayEquals(new String[] {"4", "3"}, list.get(11));
// 校验数量一致
assertEquals(Arrangement.count(4, 2), list.size());
// 逐项严格校验顺序是否一致 DFS 顺序
assertArrayEquals(new String[]{"1", "2"}, list.get(0));
assertArrayEquals(new String[]{"1", "3"}, list.get(1));
assertArrayEquals(new String[]{"1", "4"}, list.get(2));
assertArrayEquals(new String[]{"2", "1"}, list.get(3));
assertArrayEquals(new String[]{"2", "3"}, list.get(4));
assertArrayEquals(new String[]{"2", "4"}, list.get(5));
assertArrayEquals(new String[]{"3", "1"}, list.get(6));
assertArrayEquals(new String[]{"3", "2"}, list.get(7));
assertArrayEquals(new String[]{"3", "4"}, list.get(8));
assertArrayEquals(new String[]{"4", "1"}, list.get(9));
assertArrayEquals(new String[]{"4", "2"}, list.get(10));
assertArrayEquals(new String[]{"4", "3"}, list.get(11));
// 测试 selectAll
List<String[]> selectAll = arrangement.selectAll();
assertEquals(Arrangement.countAll(4), selectAll.size());
// m=0应该返回一个空排列
List<String[]> list2 = arrangement.select(0);
assertEquals(1, list2.size());
assertEquals(0, list2.get(0).length);
}
// ----------------------------------------------------
// 扩展测试边界错误处理
// ----------------------------------------------------
@Test
public void boundaryTest() {
Arrangement arr = new Arrangement(new String[]{"A", "B", "C"});
// m = n
List<String[]> full = arr.select(3);
assertEquals(6, full.size());
// m = 1
List<String[]> one = arr.select(1);
assertEquals(3, one.size());
assertArrayEquals(new String[]{"A"}, one.get(0));
// m > n empty list
assertTrue(arr.select(10).isEmpty());
// m < 0 empty list
assertTrue(arr.select(-1).isEmpty());
}
// ----------------------------------------------------
// 扩展测试空数组
// ----------------------------------------------------
@Test
public void emptyTest() {
Arrangement arrangement = new Arrangement(new String[]{});
assertEquals(1, arrangement.select(0).size());
assertTrue(arrangement.select(1).isEmpty());
assertTrue(arrangement.selectAll().isEmpty()); // A(0,m) = 0 for m>0A(0,0)=1 全排列 = 1 个空排列
}
// ----------------------------------------------------
// 扩展测试重复元素用于验证去重算法
// 默认 Arrangement 不去重因此应该包含重复排列
// ----------------------------------------------------
@Test
@Disabled("默认 Arrangement 不支持去重;启用后手动检查")
public void duplicateElementTest() {
Arrangement arrangement = new Arrangement(new String[]{"1", "1", "3"});
List<String[]> list = arrangement.select(2);
// 应该有 A(3,2) = 6
assertEquals(6, list.size());
for (String[] s : list) {
Console.log(s);
}
}
// ----------------------------------------------------
// 扩展测试selectAll 覆盖全部不重复排列A(n,1..n)
// ----------------------------------------------------
@Test
public void selectAllTest() {
Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3"});
List<String[]> all = arrangement.selectAll();
// 打印用于观测
for (String[] s : all) {
Console.log(s);
}
// A(3,1) + A(3,2) + A(3,3) = 3 + 6 + 6 = 15
assertEquals(Arrangement.countAll(3), all.size());
assertEquals(15, all.size());
// spot check 不重复排列
assertArrayEquals(new String[]{"1"}, all.get(0));
assertArrayEquals(new String[]{"1", "2"}, all.get(3));
assertArrayEquals(new String[]{"1", "2", "3"}, all.get(9));
}
// ----------------------------------------------------
// 迭代器测试
// ----------------------------------------------------
@Test
public void iteratorTest() {
Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3"});
// 测试 m=2 的情况
List<String[]> iterResult = new ArrayList<>();
for (String[] perm : arrangement.iterate(2)) {
iterResult.add(perm);
}
assertEquals(6, iterResult.size());
assertArrayEquals(new String[]{"1", "2"}, iterResult.get(0));
assertArrayEquals(new String[]{"1", "3"}, iterResult.get(1));
assertArrayEquals(new String[]{"2", "1"}, iterResult.get(2));
assertArrayEquals(new String[]{"2", "3"}, iterResult.get(3));
assertArrayEquals(new String[]{"3", "1"}, iterResult.get(4));
assertArrayEquals(new String[]{"3", "2"}, iterResult.get(5));
}
@Test
@Disabled
public void selectTest2() {
List<String[]> list = MathUtil.arrangementSelect(new String[] { "1", "1", "3", "4" });
for (String[] strings : list) {
Console.log(strings);
public void iteratorFullTest() {
Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3"});
// 测试全排列的情况
List<String[]> iterResult = new ArrayList<>();
for (String[] perm : arrangement.iterate(3)) {
iterResult.add(perm);
}
assertEquals(6, iterResult.size());
}
@Test
public void iteratorBoundaryTest() {
Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3"});
// 测试 m > n 的情况
List<String[]> iterResult = new ArrayList<>();
for (String[] perm : arrangement.iterate(5)) {
iterResult.add(perm);
}
assertTrue(iterResult.isEmpty());
}
}

View File

@ -1,10 +1,12 @@
package cn.hutool.core.math;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* 组合单元测试
*
@ -51,4 +53,96 @@ public class CombinationTest {
List<String[]> list2 = combination.select(0);
assertEquals(1, list2.size());
}
// -----------------------------
// countBig() 正确性测试
// -----------------------------
@Test
void testCountBig_basicCases() {
assertEquals(BigInteger.ONE, Combination.countBig(5, 0));
assertEquals(BigInteger.ONE, Combination.countBig(5, 5));
assertEquals(BigInteger.valueOf(10), Combination.countBig(5, 3));
assertEquals(BigInteger.valueOf(10), Combination.countBig(5, 2));
}
@Test
void testCountBig_mGreaterThanN() {
assertEquals(BigInteger.ZERO, Combination.countBig(5, 6));
}
@Test
void testCountBig_negativeInput() {
assertThrows(IllegalArgumentException.class, () -> Combination.countBig(-1, 3));
assertThrows(IllegalArgumentException.class, () -> Combination.countBig(5, -2));
}
@Test
void testCountBig_symmetry() {
assertEquals(Combination.countBig(20, 3), Combination.countBig(20, 17));
}
@Test
void testCountBig_largeNumbers() {
// C(50, 3) = 19600
assertEquals(new BigInteger("19600"), Combination.countBig(50, 3));
// C(100, 50) 的确切值重要测试
BigInteger expected = new BigInteger(
"100891344545564193334812497256"
);
assertEquals(expected, Combination.countBig(100, 50));
}
@Test
void testCountBig_veryLargeCombination() {
// 不比较具体值只断言不要抛错
BigInteger result = Combination.countBig(2000, 1000);
assertTrue(result.signum() > 0);
}
// -----------------------------
// count(long) 兼容性测试
// -----------------------------
@Test
void testCount_basic() {
assertEquals(10L, Combination.count(5, 3));
assertEquals(1L, Combination.count(5, 0));
assertEquals(0L, Combination.count(5, 6));
}
@Test
void testCount_overflowBehavior() {
// C(100, 50) 远超 long 范围但旧版行为要求不抛异常
long r = Combination.count(100, 50);
// longValue() 不抛异常并且可能溢出
assertNotNull(r);
}
@Test
void testCount_noException() {
assertDoesNotThrow(() -> Combination.count(5000, 2500));
}
// -----------------------------
// countSafe() 安全 long 版本测试
// -----------------------------
@Test
void testCountSafe_exactFitsLong() {
// C(50, 3) = 19600 fits long
assertEquals(19600L, Combination.countSafe(50, 3));
}
@Test
void testCountSafe_overflowThrows() {
// C(100, 50) 超出 long 应抛 ArithmeticException
assertThrows(ArithmeticException.class, () -> Combination.countSafe(100, 50));
}
@Test
void testCountSafe_invalidInput() {
assertThrows(IllegalArgumentException.class, () -> Combination.countSafe(-1, 3));
assertThrows(IllegalArgumentException.class, () -> Combination.countSafe(3, -1));
}
}

View File

@ -1,8 +1,9 @@
package cn.hutool.core.text;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class PasswdStrengthTest {
@Test
public void strengthTest(){
@ -15,4 +16,25 @@ public class PasswdStrengthTest {
String passwd = "9999999999999";
assertEquals(0, PasswdStrength.check(passwd));
}
@Test
public void consecutiveLettersTest() {
// 测试连续小写字母会被降级
assertEquals(0, PasswdStrength.check("abcdefghijklmn"));
// 测试连续大写字母会被降级
assertEquals(0, PasswdStrength.check("ABCDEFGHIJKLMN"));
}
@Test
public void dictionaryWeakPasswordTest() {
// 测试包含简单密码字典中的弱密码
assertEquals(0, PasswdStrength.check("password"));
assertEquals(3, PasswdStrength.check("password2"));
}
@Test
public void numericSequenceTest() {
assertEquals(0, PasswdStrength.check("01234567890"));
assertEquals(0, PasswdStrength.check("09876543210"));
}
}

View File

@ -6,6 +6,8 @@ import cn.hutool.core.text.finder.PatternFinder;
import cn.hutool.core.text.finder.StrFinder;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
@ -153,4 +155,18 @@ public class SplitIterTest {
assertEquals(1, strings.size());
});
}
@Test
public void issue4169Test() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 20000; i++) { // 1万次连续分隔符模拟递归深度风险场景
sb.append(",");
}
sb.append("test");
SplitIter iter = new SplitIter(sb.toString(), new StrFinder(",",false), 0, true);
List<String> result = iter.toList(false);
assertEquals(Collections.singletonList("test"), result);
}
}

View File

@ -66,4 +66,126 @@ public class EscapeUtilTest {
final String s = EscapeUtil.unescapeHtml4(str);
assertEquals("'some text with single quotes'", s);
}
@Test
public void escapeXmlTest(){
final String a = "<>";
final String escape = EscapeUtil.escapeXml(a);
assertEquals("&lt;&gt;", escape);
assertEquals("中文“双引号”", EscapeUtil.escapeXml("中文“双引号”"));
}
@Test
void testUnescapeNull() {
assertNull(EscapeUtil.unescape(null));
}
@Test
void testUnescapeEmpty() {
assertEquals("", EscapeUtil.unescape(""));
}
@Test
void testUnescapeBlank() {
assertEquals(" ", EscapeUtil.unescape(" "));
}
@Test
void testUnescapeAsciiCharacters() {
// 测试ASCII字符转义
assertEquals("hello", EscapeUtil.unescape("hello"));
assertEquals("test space", EscapeUtil.unescape("test%20space"));
assertEquals("A", EscapeUtil.unescape("%41"));
assertEquals("a", EscapeUtil.unescape("%61"));
assertEquals("0", EscapeUtil.unescape("%30"));
assertEquals("!", EscapeUtil.unescape("%21"));
assertEquals("@", EscapeUtil.unescape("%40"));
assertEquals("#", EscapeUtil.unescape("%23"));
}
@Test
void testUnescapeUnicodeCharacters() {
// 测试Unicode字符转义
assertEquals("", EscapeUtil.unescape("%u4E2D"));
assertEquals("", EscapeUtil.unescape("%u6587"));
assertEquals("", EscapeUtil.unescape("%u6D4B"));
assertEquals("", EscapeUtil.unescape("%u8BD5"));
assertEquals("😊", EscapeUtil.unescape("%uD83D%uDE0A")); // 笑脸表情
}
@Test
void testUnescapeMixedContent() {
// 测试混合内容
assertEquals("Hello 世界!", EscapeUtil.unescape("Hello%20%u4E16%u754C%21"));
assertEquals("测试: 100%", EscapeUtil.unescape("%u6D4B%u8BD5%3A%20100%25"));
assertEquals("a+b=c", EscapeUtil.unescape("a%2Bb%3Dc"));
}
@Test
void testUnescapeIncompleteEscapeSequences() {
// 测试不完整的转义序列
assertEquals("test%", EscapeUtil.unescape("test%"));
assertEquals("test%u", EscapeUtil.unescape("test%u"));
assertEquals("test%u1", EscapeUtil.unescape("test%u1"));
assertEquals("test%u12", EscapeUtil.unescape("test%u12"));
assertEquals("test%u123", EscapeUtil.unescape("test%u123"));
assertEquals("test%1", EscapeUtil.unescape("test%1"));
assertEquals("test%2", EscapeUtil.unescape("test%2"));
}
@Test
void testUnescapeEdgeCases() {
// 测试边界情况
assertEquals("%", EscapeUtil.unescape("%"));
assertEquals("%u", EscapeUtil.unescape("%u"));
assertEquals("%%", EscapeUtil.unescape("%%"));
assertEquals("%u%", EscapeUtil.unescape("%u%"));
assertEquals("100% complete", EscapeUtil.unescape("100%25%20complete"));
}
@Test
void testUnescapeMultipleEscapeSequences() {
// 测试多个连续的转义序列
assertEquals("ABC", EscapeUtil.unescape("%41%42%43"));
assertEquals("中文测试", EscapeUtil.unescape("%u4E2D%u6587%u6D4B%u8BD5"));
assertEquals("A 中 B", EscapeUtil.unescape("%41%20%u4E2D%20%42"));
}
@Test
void testUnescapeSpecialCharacters() {
// 测试特殊字符
assertEquals("\n", EscapeUtil.unescape("%0A"));
assertEquals("\r", EscapeUtil.unescape("%0D"));
assertEquals("\t", EscapeUtil.unescape("%09"));
assertEquals(" ", EscapeUtil.unescape("%20"));
assertEquals("<", EscapeUtil.unescape("%3C"));
assertEquals(">", EscapeUtil.unescape("%3E"));
assertEquals("&", EscapeUtil.unescape("%26"));
}
@Test
void testUnescapeComplexScenario() {
// 测试复杂场景
final String original = "Hello 世界! 这是测试。Email: test@example.com";
final String escaped = "Hello%20%u4E16%u754C%21%20%u8FD9%u662F%u6D4B%u8BD5%u3002Email%3A%20test%40example.com";
assertEquals(original, EscapeUtil.unescape(escaped));
}
@Test
void testUnescapeWithIncompleteAtEnd() {
// 测试末尾有不完整转义序列
assertEquals("normal%", EscapeUtil.unescape("normal%"));
assertEquals("normal%u", EscapeUtil.unescape("normal%u"));
assertEquals("normal%u1", EscapeUtil.unescape("normal%u1"));
assertEquals("normal%1", EscapeUtil.unescape("normal%1"));
}
@Test
void testUnescapeUppercaseHex() {
// 测试大写十六进制
assertEquals("A", EscapeUtil.unescape("%41"));
assertEquals("A", EscapeUtil.unescape("%41"));
assertEquals("", EscapeUtil.unescape("%u4E2D"));
assertEquals("", EscapeUtil.unescape("%u4E2D"));
}
}

View File

@ -3,6 +3,7 @@ package cn.hutool.core.util;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
/**
@ -85,4 +86,65 @@ public class HexUtilTest {
final String s1 = HexUtil.decodeHexStr(s);
assertEquals("6", s1);
}
@Test
public void hexToIntTest() {
final String hex1 = "FF";
assertEquals(255, HexUtil.hexToInt(hex1));
final String hex2 = "0xFF";
assertEquals(255, HexUtil.hexToInt(hex2));
final String hex3 = "#FF";
assertEquals(255, HexUtil.hexToInt(hex3));
}
@Test
public void hexToLongTest() {
final String hex1 = "FF";
assertEquals(255L, HexUtil.hexToLong(hex1));
final String hex2 = "0xFF";
assertEquals(255L, HexUtil.hexToLong(hex2));
final String hex3 = "#FF";
assertEquals(255L, HexUtil.hexToLong(hex3));
}
@Test
public void toBigIntegerTest() {
final String hex1 = "FF";
assertEquals(new BigInteger("FF", 16), HexUtil.toBigInteger(hex1));
final String hex2 = "0xFF";
assertEquals(new BigInteger("FF", 16), HexUtil.toBigInteger(hex2));
final String hex3 = "#FF";
assertEquals(new BigInteger("FF", 16), HexUtil.toBigInteger(hex3));
}
@Test
public void testFormatEmpty() {
String result = HexUtil.format("");
assertEquals("", result);
}
@Test
public void testFormatSingleChar() {
String result = HexUtil.format("1");
assertEquals("1", result);
}
@Test
public void testFormatOddLength() {
String result = HexUtil.format("123");
assertEquals("12 3", result);
}
@Test
public void testFormatWithPrefixSingleChar() {
String result = HexUtil.format("1", "0x");
assertEquals("0x1", result);
}
@Test
public void testFormatWithPrefixOddLength() {
String result = HexUtil.format("123", "0x");
assertEquals("0x12 0x3", result);
}
}

View File

@ -4,13 +4,12 @@ import cn.hutool.core.clone.CloneSupport;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
public class ObjectUtilTest {
@ -108,4 +107,74 @@ public class ObjectUtilTest {
String a = null;
assertFalse(ObjectUtil.isNotNull(a));
}
@Test
public void testLengthConsumesIterator() {
List<String> list = Arrays.asList("a", "b", "c");
Iterator<String> iterator = list.iterator();
// 迭代器第一次调用length
int length1 = ObjectUtil.length(iterator);
assertEquals(3, length1);
// 迭代器第二次调用length - 迭代器已经被消耗返回0
int length2 = ObjectUtil.length(iterator);
assertEquals(0, length2); // 但当前实现会重新遍历但iterator已经没有元素了
// 尝试使用迭代器 - 已经无法使用
assertFalse(iterator.hasNext());
}
@Test
public void testLengthConsumesEnumeration() {
Vector<String> vector = new Vector<>(Arrays.asList("a", "b", "c"));
Enumeration<String> enumeration = vector.elements();
// 第一次调用length
int length1 = ObjectUtil.length(enumeration);
assertEquals(3, length1);
// 第二次调用length - 枚举已经被消耗
int length2 = ObjectUtil.length(enumeration);
assertEquals(0, length2);
// 枚举已经无法使用
assertFalse(enumeration.hasMoreElements());
}
@Test
public void testContainsElementToStringReturnsNull() {
Object problematicElement = new Object() {
@Override
public String toString() {
return null; // 返回 null toString
}
};
assertFalse(ObjectUtil.contains("test", problematicElement)); //不会抛异常
}
@Test
public void testContainsElementToStringInvalidSyntax() {
//字符串包含自定义User对象不符合语义
assertFalse(ObjectUtil.contains("User[id=123]", new User(123)));
}
static class User{
private int id;
public User(int id) {
this.id = id;
}
@Override
public String toString() {
return "User[" +
"id=" + id +
']';
}
}
@Test
public void testContainsCharSequenceSupported() {
//contains方法支持StringStringBuilderStringBuffer
StringBuilder stringBuilder = new StringBuilder("hello world");
StringBuffer stringBuffer = new StringBuffer("hello world");
String str = "hello world";
assertTrue((ObjectUtil.contains(stringBuilder, "world")));
assertTrue(ObjectUtil.contains(stringBuffer, "hello"));
assertTrue(ObjectUtil.contains(str, "hello"));
}
}

View File

@ -210,6 +210,7 @@ public class ReflectUtilTest {
private String n;
}
@SuppressWarnings("UnusedReturnValue")
public static Method getMethodWithReturnTypeCheck(final Class<?> clazz, final boolean ignoreCase, final String methodName, final Class<?>... paramTypes) throws SecurityException {
if (null == clazz || StrUtil.isBlank(methodName)) {
return null;
@ -300,6 +301,7 @@ public class ReflectUtilTest {
}
class C2 extends C1 {
@SuppressWarnings("RedundantMethodOverride")
@Override
public void getA() {
@ -307,7 +309,7 @@ public class ReflectUtilTest {
}
@Test
public void newInstanceIfPossibleTest(){
public void newInstanceIfPossibleTest() {
//noinspection ConstantConditions
final int intValue = ReflectUtil.newInstanceIfPossible(int.class);
assertEquals(0, intValue);
@ -351,24 +353,63 @@ public class ReflectUtilTest {
}
@Test
public void issue2625Test(){
public void issue2625Test() {
// 内部类继承的情况下父类方法会被定义为桥接方法因此按照pr#1965@Github判断返回值的继承关系来代替判断桥接
final Method getThis = ReflectUtil.getMethod(A.C.class, "getThis");
assertTrue(getThis.isBridge());
}
@SuppressWarnings("InnerClassMayBeStatic")
public class A{
public class A {
public class C extends B{
public class C extends B {
}
protected class B{
public B getThis(){
protected class B {
public B getThis() {
return this;
}
}
}
@Test
public void newInstanceIfPossibleTest2() {
// 测试Object.class不应该被错误地实例化为HashMap应该返回Object实例
Object objectInstance = ReflectUtil.newInstanceIfPossible(Object.class);
assertNotNull(objectInstance);
assertEquals(Object.class, objectInstance.getClass());
// 测试Map.class能够正确实例化为HashMap
Map<?, ?> mapInstance = ReflectUtil.newInstanceIfPossible(Map.class);
assertNotNull(mapInstance);
assertInstanceOf(HashMap.class, mapInstance);
// 测试Collection.class能够正确实例化为ArrayList
Collection<?> collectionInstance = ReflectUtil.newInstanceIfPossible(Collection.class);
assertNotNull(collectionInstance);
assertInstanceOf(ArrayList.class, collectionInstance);
// 测试List.class能够正确实例化为ArrayList
List<?> listInstance = ReflectUtil.newInstanceIfPossible(List.class);
assertNotNull(listInstance);
assertInstanceOf(ArrayList.class, listInstance);
// 测试Set.class能够正确实例化为HashSet
Set<?> setInstance = ReflectUtil.newInstanceIfPossible(Set.class);
assertNotNull(setInstance);
assertInstanceOf(HashSet.class, setInstance);
// 测试Queue接口能够正确实例化为LinkedList
Queue<?> queueInstance = ReflectUtil.newInstanceIfPossible(Queue.class);
assertNotNull(queueInstance);
assertInstanceOf(LinkedList.class, queueInstance);
// 测试Deque接口能够正确实例化为LinkedList
Deque<?> dequeInstance = ReflectUtil.newInstanceIfPossible(Deque.class);
assertNotNull(dequeInstance);
assertInstanceOf(LinkedList.class, dequeInstance);
}
}

View File

@ -1,5 +1,7 @@
package cn.hutool.core.util;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
@ -38,7 +40,7 @@ public class TypeUtilTest {
public void getClasses() {
Method method = ReflectUtil.getMethod(Parent.class, "getLevel");
Type returnType = TypeUtil.getReturnType(method);
Class clazz = TypeUtil.getClass(returnType);
Class<?> clazz = TypeUtil.getClass(returnType);
assertEquals(Level1.class, clazz);
method = ReflectUtil.getMethod(Level1.class, "getId");
@ -47,6 +49,37 @@ public class TypeUtilTest {
assertEquals(Object.class, clazz);
}
/**
* 测试getClass方法对泛型数组类型T[]的处理
* 验证未绑定泛型参数的数组类型会被正确解析为Object[]
*/
@Test
public void getClassForGenericArrayTypeTest() throws NoSuchFieldException {
// 获取T[]类型字段的泛型类型
Field levelField = GenericArray.class.getDeclaredField("level");
Type genericArrayType = levelField.getGenericType();
// 调用getClass方法处理GenericArrayType
Class<?> clazz = TypeUtil.getClass(genericArrayType);
// 验证返回Object[]类型
assertNotNull(clazz, "getClass方法返回null");
assertTrue(clazz.isArray(), "返回类型不是数组");
assertEquals(Object.class, clazz.getComponentType(), "数组组件类型应为Object");
}
/**
* 测试getClass方法对参数化类型数组{@code List<String>[]}的处理
* 验证数组组件类型能正确解析为原始类型
*/
@Test
public void getClassForParameterizedArrayTypeTest() {
// 创建List<String>[]类型引用
Type genericArrayType = new TypeReference<List<String>[]>() {}.getType();
// 调用getClass方法处理GenericArrayType
Class<?> clazz = TypeUtil.getClass(genericArrayType);
// 验证返回List[]类型
assertEquals(Array.newInstance(List.class, 0).getClass(), clazz);
}
public static class TestClass {
public List<String> getList() {
return new ArrayList<>();

View File

@ -76,4 +76,32 @@ class VersionUtilTest {
@Test
void testMatchEl() {
}
/**
* 测试版本范围表达式边界情况
* 1. 左边界为空的情况: "-1.0.3" 应该匹配小于等于1.0.3的版本
* 2. 右边界为空的情况: "1.0.0-" 应该匹配大于等于1.0.0的版本
* 3. 双边界为空的情况: "-" 应该匹配所有版本
* 验证 VersionUtil.matchEl 方法对边界值的正确处理
*/
@Test
void matchEl_rangeBoundaryCases() {
String currentVersion = "1.0.2";
// 测试左边界为空的情况: "-1.0.3" 应该匹配小于等于1.0.3的版本
assertTrue(VersionUtil.matchEl(currentVersion, "-1.0.3"));
assertTrue(VersionUtil.matchEl(currentVersion, "-1.0.2"));
assertFalse(VersionUtil.matchEl(currentVersion, "-1.0.0"));
// 测试右边界为空的情况: "1.0.0-" 应该匹配大于等于1.0.0的版本
assertTrue(VersionUtil.matchEl(currentVersion, "1.0.0-"));
assertTrue(VersionUtil.matchEl(currentVersion, "1.0.2-"));
assertFalse(VersionUtil.matchEl(currentVersion, "1.0.3-"));
// 测试双边为空的情况: "-" 应该匹配所有版本
assertTrue(VersionUtil.matchEl(currentVersion, "-"));
assertTrue(VersionUtil.matchEl("0.0.1", "-"));
assertTrue(VersionUtil.matchEl("999.999.999", "-"));
}
}

View File

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

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.41</version>
<version>5.8.42</version>
</parent>
<artifactId>hutool-crypto</artifactId>
@ -18,8 +18,6 @@
<properties>
<Automatic-Module-Name>cn.hutool.crypto</Automatic-Module-Name>
<!-- versions -->
<bouncycastle.version>1.78.1</bouncycastle.version>
</properties>
<dependencies>

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.41</version>
<version>5.8.42</version>
</parent>
<artifactId>hutool-db</artifactId>
@ -151,7 +151,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>12.2.0.jre8</version>
<version>13.2.1.jre8</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@ -133,7 +133,7 @@ public interface Dialect extends Serializable {
* @throws SQLException SQL执行异常
*/
default PreparedStatement psForCount(Connection conn, Query query) throws SQLException {
return psForCount(conn, SqlBuilder.create().query(query));
return psForCount(conn, SqlBuilder.create(getWrapper()).query(query));
}
/**

View File

@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -125,4 +126,27 @@ public class NamedSqlTest {
assertEquals("select * from user where comment = 'include in text' and id = ?", namedSql.getSql());
assertArrayEquals(new int[]{5, 6}, (int[]) namedSql.getParams()[0]);
}
@Test
void selectCaseInTest() {
final HashMap<String, Object> paramMap = MapUtil.of("number", new int[]{1, 2, 3});
NamedSql namedSql = new NamedSql("select case when 2 = any(ARRAY[:number]) and 1 in (1) then 1 else 0 end", paramMap);
assertEquals("select case when 2 = any(ARRAY[?]) and 1 in (1) then 1 else 0 end", namedSql.getSql());
assertArrayEquals(new int[]{1, 2, 3}, (int[])namedSql.getParams()[0]);
}
@Test
public void parseInsertMultiRowTest() {
// 多行 INSERT 语句
final Map<String, Object> paramMap = new LinkedHashMap<>();
paramMap.put("user1", new Object[]{1, "looly"});
paramMap.put("user2", new Object[]{2, "xxxtea"});
String sql = "INSERT INTO users (id, name) VALUES (:user1), (:user2)";
NamedSql namedSql = new NamedSql(sql, paramMap);
assertEquals("INSERT INTO users (id, name) VALUES (?), (?)", namedSql.getSql());
assertArrayEquals(new Object[]{new Object[]{1, "looly"}, new Object[]{2, "xxxtea"}}, namedSql.getParams());
}
}

View File

@ -1,12 +1,16 @@
package cn.hutool.db;
import java.sql.SQLException;
import static org.junit.jupiter.api.Assertions.*;
import cn.hutool.core.lang.Console;
import cn.hutool.core.map.MapUtil;
import cn.hutool.db.sql.NamedSql;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import cn.hutool.core.lang.Console;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* PostgreSQL 单元测试
@ -46,4 +50,14 @@ public class PostgreTest {
Entity et=db.get(Entity.create("ctest").set("id", 1));
assertEquals("new111",et.getStr("t1"));
}
@Test
@Disabled
void namedSqlWithInTest() throws SQLException {
final HashMap<String, Object> paramMap = MapUtil.of("number", new int[]{1, 2, 3});
NamedSql namedSql = new NamedSql("select case when 2 = any(ARRAY[:number]) and 1 in (1) then 1 else 0 end", paramMap);
final Db db = Db.use("postgre");
final List<Entity> query = db.query(namedSql.getSql(), namedSql.getParams());
Console.log(query);
}
}

View File

@ -59,7 +59,7 @@ pass = 123456
remarks = true
[postgre]
url = jdbc:postgresql://looly.centos:5432/test_hutool
url = jdbc:postgresql://localhost:5432/test_hutool
user = postgres
pass = 123456
remarks = true

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import cn.hutool.core.lang.SimpleCache;
import cn.hutool.core.util.StrUtil;
import com.jcraft.jsch.Session;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
/**
@ -110,15 +110,17 @@ public enum JschSessionPool {
*/
public void remove(Session session) {
if (null != session) {
final Iterator<Entry<String, Session>> iterator = this.cache.iterator();
Entry<String, Session> entry;
while (iterator.hasNext()) {
entry = iterator.next();
String key = null;
for (Map.Entry<String, Session> entry : cache) {
if (session.equals(entry.getValue())) {
iterator.remove();
key = entry.getKey();
break;
}
}
if(null != key){
cache.remove(key);
}
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.41</version>
<version>5.8.42</version>
</parent>
<artifactId>hutool-http</artifactId>
@ -38,5 +38,11 @@
<version>1.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.soap</groupId>
<artifactId>jakarta.xml.soap-api</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -598,10 +598,11 @@ public class HttpConnection {
}
/**
* 通过反射设置方法名首先设置HttpURLConnection本身的方法名再检查是否为代理类如果是设置带路对象的方法名
* 通过反射设置方法名首先设置HttpURLConnection本身的方法名再检查是否为代理类如果是设置代理对象的方法名
* @param method 方法名
*/
private void reflectSetMethod(Method method){
try {
ReflectUtil.setFieldValue(this.conn, "method", method.name());
// HttpsURLConnectionImpl实现中使用了代理类需要修改被代理类的method方法
@ -609,6 +610,10 @@ public class HttpConnection {
if(null != delegate){
ReflectUtil.setFieldValue(delegate, "method", method.name());
}
} catch (Exception e){
// ignore
// https://github.com/chinabugotech/hutool/issues/4109
}
}
// --------------------------------------------------------------- Private Method end
}

View File

@ -0,0 +1,654 @@
package cn.hutool.http.webservice;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.XmlUtil;
import cn.hutool.http.HttpBase;
import cn.hutool.http.HttpGlobalConfig;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import jakarta.xml.soap.*;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* SOAP客户端
*
* <p>
* 此对象用于构建一个SOAP消息并通过HTTP接口发出消息内容
* SOAP消息本质上是一个XML文本可以通过调用{@link #getMsgStr(boolean)} 方法获取消息体
* <p>
* 使用方法
*
* <pre>
* SoapClient client = SoapClient.create(url)
* .setMethod(methodName, namespaceURI)
* .setCharset(CharsetUtil.CHARSET_GBK)
* .setParam(param1, "XXX");
*
* String response = client.send(true);
*
* </pre>
*
* @author looly
* @since 5.8.42
*/
public class JakartaSoapClient extends HttpBase<JakartaSoapClient> {
/**
* XML消息体的Content-Type
* soap1.1 : text/xml
* soap1.2 : application/soap+xml
* soap1.1与soap1.2区别: https://www.cnblogs.com/qlqwjy/p/7577147.html
*/
private static final String CONTENT_TYPE_SOAP11_TEXT_XML = "text/xml;charset=";
private static final String CONTENT_TYPE_SOAP12_SOAP_XML = "application/soap+xml;charset=";
/**
* 请求的URL地址
*/
private String url;
/**
* 默认连接超时
*/
private int connectionTimeout = HttpGlobalConfig.getTimeout();
/**
* 默认读取超时
*/
private int readTimeout = HttpGlobalConfig.getTimeout();
/**
* 消息工厂用于创建消息
*/
private MessageFactory factory;
/**
* SOAP消息
*/
private SOAPMessage message;
/**
* 消息方法节点
*/
private SOAPBodyElement methodEle;
/**
* 应用于方法上的命名空间URI
*/
private final String namespaceURI;
/**
* Soap协议
* soap1.1 : text/xml
* soap1.2 : application/soap+xml
*/
private final SoapProtocol protocol;
/**
* 创建SOAP客户端默认使用soap1.1版本协议
*
* @param url WS的URL地址
* @return this
*/
public static JakartaSoapClient create(String url) {
return new JakartaSoapClient(url);
}
/**
* 创建SOAP客户端
*
* @param url WS的URL地址
* @param protocol 协议{@link SoapProtocol}
* @return this
*/
public static JakartaSoapClient create(String url, SoapProtocol protocol) {
return new JakartaSoapClient(url, protocol);
}
/**
* 创建SOAP客户端
*
* @param url WS的URL地址
* @param protocol 协议{@link SoapProtocol}
* @param namespaceURI 方法上的命名空间URI
* @return this
* @since 4.5.6
*/
public static JakartaSoapClient create(String url, SoapProtocol protocol, String namespaceURI) {
return new JakartaSoapClient(url, protocol, namespaceURI);
}
/**
* 构造默认使用soap1.1版本协议
*
* @param url WS的URL地址
*/
public JakartaSoapClient(String url) {
this(url, SoapProtocol.SOAP_1_1);
}
/**
* 构造
*
* @param url WS的URL地址
* @param protocol 协议版本{@link SoapProtocol}
*/
public JakartaSoapClient(String url, SoapProtocol protocol) {
this(url, protocol, null);
}
/**
* 构造
*
* @param url WS的URL地址
* @param protocol 协议版本{@link SoapProtocol}
* @param namespaceURI 方法上的命名空间URI
* @since 4.5.6
*/
public JakartaSoapClient(String url, SoapProtocol protocol, String namespaceURI) {
this.url = url;
this.namespaceURI = namespaceURI;
this.protocol = protocol;
init(protocol);
}
/**
* 初始化
*
* @param protocol 协议版本枚举{@link SoapProtocol}
* @return this
*/
public JakartaSoapClient init(SoapProtocol protocol) {
// 创建消息工厂
try {
this.factory = MessageFactory.newInstance(protocol.getValue());
// 根据消息工厂创建SoapMessage
this.message = factory.createMessage();
} catch (SOAPException e) {
throw new SoapRuntimeException(e);
}
return this;
}
/**
* 重置SOAP客户端用于客户端复用
*
* <p>
* 重置后需调用serMethod方法重新指定请求方法并调用setParam方法重新定义参数
*
* @return this
* @since 4.6.7
*/
public JakartaSoapClient reset() {
try {
this.message = factory.createMessage();
} catch (SOAPException e) {
throw new SoapRuntimeException(e);
}
this.methodEle = null;
return this;
}
/**
* 设置编码
*
* @param charset 编码
* @return this
* @see #charset(Charset)
*/
public JakartaSoapClient setCharset(Charset charset) {
return this.charset(charset);
}
@Override
public JakartaSoapClient charset(Charset charset) {
super.charset(charset);
try {
this.message.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, this.charset());
this.message.setProperty(SOAPMessage.WRITE_XML_DECLARATION, "true");
} catch (SOAPException e) {
// ignore
}
return this;
}
/**
* 设置Webservice请求地址
*
* @param url Webservice请求地址
* @return this
*/
public JakartaSoapClient setUrl(String url) {
this.url = url;
return this;
}
/**
* 增加SOAP头信息方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
*
* @param name 头信息标签名
* @param actorURI 中间的消息接收者
* @param roleUri Role的URI
* @param mustUnderstand 标题项对于要对其进行处理的接收者来说是强制的还是可选的
* @param relay relay属性
* @return {@link SOAPHeaderElement}
* @since 5.4.4
*/
public SOAPHeaderElement addSOAPHeader(QName name, String actorURI, String roleUri, Boolean mustUnderstand, Boolean relay) {
final SOAPHeaderElement ele = addSOAPHeader(name);
try {
if (StrUtil.isNotBlank(roleUri)) {
ele.setRole(roleUri);
}
if (null != relay) {
ele.setRelay(relay);
}
} catch (SOAPException e) {
throw new SoapRuntimeException(e);
}
if (StrUtil.isNotBlank(actorURI)) {
ele.setActor(actorURI);
}
if (null != mustUnderstand) {
ele.setMustUnderstand(mustUnderstand);
}
return ele;
}
/**
* 增加SOAP头信息方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
*
* @param localName 头节点名称
* @return {@link SOAPHeaderElement}
* @since 5.4.7
*/
public SOAPHeaderElement addSOAPHeader(String localName) {
return addSOAPHeader(new QName(localName));
}
/**
* 增加SOAP头信息方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
*
* @param localName 头节点名称
* @param value 头节点的值
* @return {@link SOAPHeaderElement}
* @since 5.4.7
*/
public SOAPHeaderElement addSOAPHeader(String localName, String value) {
final SOAPHeaderElement soapHeaderElement = addSOAPHeader(localName);
soapHeaderElement.setTextContent(value);
return soapHeaderElement;
}
/**
* 增加SOAP头信息方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
*
* @param name 头节点名称
* @return {@link SOAPHeaderElement}
* @since 5.4.4
*/
public SOAPHeaderElement addSOAPHeader(QName name) {
SOAPHeaderElement ele;
try {
ele = this.message.getSOAPHeader().addHeaderElement(name);
} catch (SOAPException e) {
throw new SoapRuntimeException(e);
}
return ele;
}
/**
* 设置请求方法
*
* @param name 方法名及其命名空间
* @param params 参数
* @param useMethodPrefix 是否使用方法的命名空间前缀
* @return this
*/
public JakartaSoapClient setMethod(Name name, Map<String, Object> params, boolean useMethodPrefix) {
return setMethod(new QName(name.getURI(), name.getLocalName(), name.getPrefix()), params, useMethodPrefix);
}
/**
* 设置请求方法
*
* @param name 方法名及其命名空间
* @param params 参数
* @param useMethodPrefix 是否使用方法的命名空间前缀
* @return this
*/
public JakartaSoapClient setMethod(QName name, Map<String, Object> params, boolean useMethodPrefix) {
setMethod(name);
final String prefix = useMethodPrefix ? name.getPrefix() : null;
final SOAPBodyElement methodEle = this.methodEle;
for (Entry<String, Object> entry : MapUtil.wrap(params)) {
setParam(methodEle, entry.getKey(), entry.getValue(), prefix);
}
return this;
}
/**
* 设置请求方法<br>
* 方法名自动识别前缀前缀和方法名使用:分隔<br>
* 当识别到前缀后自动添加xmlns属性关联到默认的namespaceURI
*
* @param methodName 方法名
* @return this
*/
public JakartaSoapClient setMethod(String methodName) {
return setMethod(methodName, ObjectUtil.defaultIfNull(this.namespaceURI, XMLConstants.NULL_NS_URI));
}
/**
* 设置请求方法<br>
* 方法名自动识别前缀前缀和方法名使用:分隔<br>
* 当识别到前缀后自动添加xmlns属性关联到传入的namespaceURI
*
* @param methodName 方法名可有前缀也可无
* @param namespaceURI 命名空间URI
* @return this
*/
public JakartaSoapClient setMethod(String methodName, String namespaceURI) {
final List<String> methodNameList = StrUtil.split(methodName, ':');
final QName qName;
if (2 == methodNameList.size()) {
qName = new QName(namespaceURI, methodNameList.get(1), methodNameList.get(0));
} else {
qName = new QName(namespaceURI, methodName);
}
return setMethod(qName);
}
/**
* 设置请求方法
*
* @param name 方法名及其命名空间
* @return this
*/
public JakartaSoapClient setMethod(QName name) {
try {
this.methodEle = this.message.getSOAPBody().addBodyElement(name);
} catch (SOAPException e) {
throw new SoapRuntimeException(e);
}
return this;
}
/**
* 设置方法参数使用方法的前缀
*
* @param name 参数名
* @param value 参数值可以是字符串或Map或{@link SOAPElement}
* @return this
*/
public JakartaSoapClient setParam(String name, Object value) {
return setParam(name, value, true);
}
/**
* 设置方法参数
*
* @param name 参数名
* @param value 参数值可以是字符串或Map或{@link SOAPElement}
* @param useMethodPrefix 是否使用方法的命名空间前缀
* @return this
*/
public JakartaSoapClient setParam(String name, Object value, boolean useMethodPrefix) {
setParam(this.methodEle, name, value, useMethodPrefix ? this.methodEle.getPrefix() : null);
return this;
}
/**
* 批量设置参数使用方法的前缀
*
* @param params 参数列表
* @return this
* @since 4.5.6
*/
public JakartaSoapClient setParams(Map<String, Object> params) {
return setParams(params, true);
}
/**
* 批量设置参数
*
* @param params 参数列表
* @param useMethodPrefix 是否使用方法的命名空间前缀
* @return this
* @since 4.5.6
*/
public JakartaSoapClient setParams(Map<String, Object> params, boolean useMethodPrefix) {
for (Entry<String, Object> entry : MapUtil.wrap(params)) {
setParam(entry.getKey(), entry.getValue(), useMethodPrefix);
}
return this;
}
/**
* 获取方法节点<br>
* 用于创建子节点等操作
*
* @return {@link SOAPBodyElement}
* @since 4.5.6
*/
public SOAPBodyElement getMethodEle() {
return this.methodEle;
}
/**
* 获取SOAP消息对象 {@link SOAPMessage}
*
* @return {@link SOAPMessage}
* @since 4.5.6
*/
public SOAPMessage getMessage() {
return this.message;
}
/**
* 获取SOAP请求消息
*
* @param pretty 是否格式化
* @return 消息字符串
*/
public String getMsgStr(boolean pretty) {
return JakartaSoapUtil.toString(this.message, pretty, this.charset);
}
/**
* 将SOAP消息的XML内容输出到流
*
* @param out 输出流
* @return this
* @since 4.5.6
*/
public JakartaSoapClient write(OutputStream out) {
try {
this.message.writeTo(out);
} catch (SOAPException | IOException e) {
throw new SoapRuntimeException(e);
}
return this;
}
/**
* 设置超时单位毫秒<br>
* 超时包括
*
* <pre>
* 1. 连接超时
* 2. 读取响应超时
* </pre>
*
* @param milliseconds 超时毫秒数
* @return this
* @see #setConnectionTimeout(int)
* @see #setReadTimeout(int)
*/
public JakartaSoapClient timeout(int milliseconds) {
setConnectionTimeout(milliseconds);
setReadTimeout(milliseconds);
return this;
}
/**
* 设置连接超时单位毫秒
*
* @param milliseconds 超时毫秒数
* @return this
* @since 4.5.6
*/
public JakartaSoapClient setConnectionTimeout(int milliseconds) {
this.connectionTimeout = milliseconds;
return this;
}
/**
* 设置连接超时单位毫秒
*
* @param milliseconds 超时毫秒数
* @return this
* @since 4.5.6
*/
public JakartaSoapClient setReadTimeout(int milliseconds) {
this.readTimeout = milliseconds;
return this;
}
/**
* 执行Webservice请求即发送SOAP内容
*
* @return 返回结果
*/
public SOAPMessage sendForMessage() {
final HttpResponse res = sendForResponse();
final MimeHeaders headers = new MimeHeaders();
for (Entry<String, List<String>> entry : res.headers().entrySet()) {
if (StrUtil.isNotEmpty(entry.getKey())) {
headers.setHeader(entry.getKey(), CollUtil.get(entry.getValue(), 0));
}
}
try {
return this.factory.createMessage(headers, res.bodyStream());
} catch (IOException | SOAPException e) {
throw new SoapRuntimeException(e);
} finally {
IoUtil.close(res);
}
}
/**
* 执行Webservice请求即发送SOAP内容
*
* @return 返回结果
*/
public String send() {
return send(false);
}
/**
* 执行Webservice请求即发送SOAP内容
*
* @param pretty 是否格式化
* @return 返回结果
*/
public String send(boolean pretty) {
final String body = sendForResponse().body();
return pretty ? XmlUtil.format(body) : body;
}
// -------------------------------------------------------------------------------------------------------- Private method start
/**
* 发送请求获取异步响应
*
* @return 响应对象
*/
public HttpResponse sendForResponse() {
return HttpRequest.post(this.url)//
.setFollowRedirects(true)//
.setConnectionTimeout(this.connectionTimeout)
.setReadTimeout(this.readTimeout)
.contentType(getXmlContentType())//
.header(this.headers())
.body(getMsgStr(false))//
.executeAsync();
}
/**
* 获取请求的Content-Type附加编码信息
*
* @return 请求的Content-Type
*/
private String getXmlContentType() {
switch (this.protocol){
case SOAP_1_1:
return CONTENT_TYPE_SOAP11_TEXT_XML.concat(this.charset.toString());
case SOAP_1_2:
return CONTENT_TYPE_SOAP12_SOAP_XML.concat(this.charset.toString());
default:
throw new SoapRuntimeException("Unsupported protocol: {}", this.protocol);
}
}
/**
* 设置方法参数
*
* @param ele 方法节点
* @param name 参数名
* @param value 参数值
* @param prefix 命名空间前缀 {@code null}表示不使用前缀
* @return {@link SOAPElement}子节点
*/
@SuppressWarnings("rawtypes")
private static SOAPElement setParam(SOAPElement ele, String name, Object value, String prefix) {
final SOAPElement childEle;
try {
if (StrUtil.isNotBlank(prefix)) {
childEle = ele.addChildElement(name, prefix);
} else {
childEle = ele.addChildElement(name);
}
} catch (SOAPException e) {
throw new SoapRuntimeException(e);
}
if (null != value) {
if (value instanceof SOAPElement) {
// 单个子节点
try {
ele.addChildElement((SOAPElement) value);
} catch (SOAPException e) {
throw new SoapRuntimeException(e);
}
} else if (value instanceof Map) {
// 多个字节点
Entry entry;
for (Object obj : ((Map) value).entrySet()) {
entry = (Entry) obj;
setParam(childEle, entry.getKey().toString(), entry.getValue(), prefix);
}
} else {
// 单个值
childEle.setValue(value.toString());
}
}
return childEle;
}
// -------------------------------------------------------------------------------------------------------- Private method end
}

View File

@ -0,0 +1,36 @@
package cn.hutool.http.webservice;
import jakarta.xml.soap.SOAPConstants;
/**
* SOAP协议版本枚举
*
* @author looly
*
*/
public enum JakartaSoapProtocol {
/** SOAP 1.1协议 */
SOAP_1_1(SOAPConstants.SOAP_1_1_PROTOCOL),
/** SOAP 1.2协议 */
SOAP_1_2(SOAPConstants.SOAP_1_2_PROTOCOL);
/**
* 构造
*
* @param value {@link SOAPConstants} 中的协议版本值
*/
JakartaSoapProtocol(String value) {
this.value = value;
}
private final String value;
/**
* 获取版本值信息
*
* @return 版本值信息
*/
public String getValue() {
return this.value;
}
}

View File

@ -0,0 +1,91 @@
package cn.hutool.http.webservice;
import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.XmlUtil;
import jakarta.xml.soap.SOAPException;
import jakarta.xml.soap.SOAPMessage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
/**
* SOAP相关工具类
*
* @author looly
* @since 4.5.7
*/
public class JakartaSoapUtil {
/**
* 创建SOAP客户端默认使用soap1.1版本协议
*
* @param url WS的URL地址
* @return {@link SoapClient}
*/
public static SoapClient createClient(String url) {
return SoapClient.create(url);
}
/**
* 创建SOAP客户端
*
* @param url WS的URL地址
* @param protocol 协议{@link SoapProtocol}
* @return {@link SoapClient}
*/
public static SoapClient createClient(String url, SoapProtocol protocol) {
return SoapClient.create(url, protocol);
}
/**
* 创建SOAP客户端
*
* @param url WS的URL地址
* @param protocol 协议{@link SoapProtocol}
* @param namespaceURI 方法上的命名空间URI
* @return {@link SoapClient}
* @since 4.5.6
*/
public static SoapClient createClient(String url, SoapProtocol protocol, String namespaceURI) {
return SoapClient.create(url, protocol, namespaceURI);
}
/**
* {@link SOAPMessage} 转为字符串
*
* @param message SOAP消息对象
* @param pretty 是否格式化
* @return SOAP XML字符串
*/
public static String toString(SOAPMessage message, boolean pretty) {
return toString(message, pretty, CharsetUtil.CHARSET_UTF_8);
}
/**
* {@link SOAPMessage} 转为字符串
*
* @param message SOAP消息对象
* @param pretty 是否格式化
* @param charset 编码
* @return SOAP XML字符串
* @since 4.5.7
*/
public static String toString(SOAPMessage message, boolean pretty, Charset charset) {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
message.writeTo(out);
} catch (SOAPException | IOException e) {
throw new SoapRuntimeException(e);
}
String messageToString;
try {
messageToString = out.toString(charset.toString());
} catch (UnsupportedEncodingException e) {
throw new UtilException(e);
}
return pretty ? XmlUtil.format(messageToString) : messageToString;
}
}

View File

@ -0,0 +1,41 @@
package cn.hutool.http.webservice;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.CharsetUtil;
import jakarta.xml.soap.SOAPException;
import jakarta.xml.soap.SOAPMessage;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
* SOAP相关单元测试
*
* @author looly
*
*/
public class JakartaSoapClientTest {
@Test
@Disabled
public void requestTest() {
JakartaSoapClient client = JakartaSoapClient.create("http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx")
.setMethod("web:getCountryCityByIp", "http://WebXml.com.cn/")
.setCharset(CharsetUtil.CHARSET_GBK)
.setParam("theIpAddress", "218.21.240.106");
Console.log(client.getMsgStr(true));
Console.log(client.send(true));
}
@Test
@Disabled
public void requestForMessageTest() throws SOAPException {
JakartaSoapClient client = JakartaSoapClient.create("http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx")
.setMethod("web:getCountryCityByIp", "http://WebXml.com.cn/")
.setParam("theIpAddress", "218.21.240.106");
SOAPMessage message = client.sendForMessage();
Console.log(message.getSOAPBody().getTextContent());
}
}

View File

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

View File

@ -359,6 +359,8 @@ public class JSONUtil {
}
if (obj instanceof CharSequence) {
return StrUtil.str((CharSequence) obj);
}else if(obj instanceof Boolean || obj instanceof Number) {
return obj.toString();
}
return toJsonStr(parse(obj, jsonConfig));
}

View File

@ -0,0 +1,14 @@
package cn.hutool.json;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class IssueID418BTest {
@Test
void booleanToJsonTest() {
Boolean dd = true;
String jsonStr = JSONUtil.toJsonStr(dd);
Assertions.assertEquals("true", jsonStr);
}
}

View File

@ -0,0 +1,19 @@
package cn.hutool.json;
import cn.hutool.core.map.MapUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Map;
public class IssueID61QRTest {
@Test
public void testName() {
JSONObject map1 = JSONUtil.createObj(new JSONConfig().setDateFormat("yyyy"));
// JSONObject map1 = JSONUtil.createObj();
map1.set("a", 3);
map1.set("b", 5);
map1.set("c", 5432);
Assertions.assertEquals("{c=5432, b=5, a=3}", MapUtil.sortByValue(JSONUtil.toBean(map1, Map.class), true).toString());
}
}

View File

@ -290,7 +290,7 @@ public class JSONUtilTest {
public void issue3540Test() {
Long userId = 10101010L;
final String jsonStr = JSONUtil.toJsonStr(userId);
assertEquals("{}", jsonStr);
assertEquals("10101010", jsonStr);
}
/**

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.8.41</version>
<version>5.8.42</version>
</parent>
<artifactId>hutool-jwt</artifactId>
@ -20,8 +20,7 @@
<Automatic-Module-Name>cn.hutool.jwt</Automatic-Module-Name>
<!-- versions -->
<bouncycastle.version>1.78.1</bouncycastle.version>
<jjwt.version>0.12.5</jjwt.version>
<jjwt.version>0.13.0</jjwt.version>
</properties>
<dependencies>

View File

@ -118,12 +118,7 @@ public class JWT implements RegisteredPayload<JWT> {
* @return this
*/
public JWT setKey(byte[] key) {
// 检查头信息中是否有算法信息
final String claim = (String) this.header.getClaim(JWTHeader.ALGORITHM);
if (StrUtil.isNotBlank(claim)) {
return setSigner(JWTSignerUtil.createSigner(claim, key));
}
return setSigner(JWTSignerUtil.hs256(key));
return setSigner(StrUtil.nullToDefault(getAlgorithm(), "HS256"), key);
}
/**
@ -169,6 +164,13 @@ public class JWT implements RegisteredPayload<JWT> {
*/
public JWT setSigner(JWTSigner signer) {
this.signer = signer;
// 检查头信息中是否有算法信息
final String algorithm = (String) this.header.getClaim(JWTHeader.ALGORITHM);
if (StrUtil.isBlank(algorithm)) {
this.header.setAlgorithm(AlgorithmUtil.getId(signer.getAlgorithm()));
}
return this;
}
@ -346,7 +348,7 @@ public class JWT implements RegisteredPayload<JWT> {
}
// 检查头信息中是否有算法信息
final String algorithm = (String) this.header.getClaim(JWTHeader.ALGORITHM);
final String algorithm = getAlgorithm();
if (StrUtil.isBlank(algorithm)) {
this.header.setClaim(JWTHeader.ALGORITHM,
AlgorithmUtil.getId(signer.getAlgorithm()));
@ -410,6 +412,16 @@ public class JWT implements RegisteredPayload<JWT> {
signer = NoneJWTSigner.NONE;
}
// 用户定义alg为none但是签名器不是NoneJWTSigner
if(NoneJWTSigner.isNone(getAlgorithm()) && !(signer instanceof NoneJWTSigner)){
throw new JWTException("Alg is 'none' but use: {} !", signer.getClass());
}
// alg非none但签名器是NoneJWTSigner
if(signer instanceof NoneJWTSigner && !NoneJWTSigner.isNone(getAlgorithm())){
throw new JWTException("Alg is not 'none' but use NoneJWTSigner!");
}
final List<String> tokens = this.tokens;
if (CollUtil.isEmpty(tokens)) {
throw new JWTException("No token to verify!");

View File

@ -34,6 +34,42 @@ public class JWTHeader extends Claims {
*/
public JWTHeader() {}
/**
* 增加alg头信息
*
* @param algorithm 算法ID如HS265
* @return this
* @since 5.8.42
*/
public JWTHeader setAlgorithm(final String algorithm) {
setClaim(ALGORITHM, algorithm);
return this;
}
/**
* 增加typ头信息
*
* @param type 类型如JWT
* @return this
* @since 5.8.42
*/
public JWTHeader setType(final String type) {
setClaim(TYPE, type);
return this;
}
/**
* 增加cty头信息
*
* @param contentType 内容类型
* @return this
* @since 5.8.42
*/
public JWTHeader setContentType(final String contentType) {
setClaim(CONTENT_TYPE, contentType);
return this;
}
/**
* 增加kid头信息
*

View File

@ -1,7 +1,7 @@
package cn.hutool.jwt.signers;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReUtil;
import cn.hutool.jwt.JWTException;
import java.security.Key;
import java.security.KeyPair;
@ -232,10 +232,12 @@ public class JWTSignerUtil {
* @return 签名器
*/
public static JWTSigner createSigner(String algorithmId, byte[] key) {
Assert.notNull(key, "Signer key must be not null!");
if (null == algorithmId || NoneJWTSigner.ID_NONE.equals(algorithmId)) {
if (NoneJWTSigner.isNone(algorithmId)) {
if(null == key){
return none();
}else{
throw new JWTException("When key is not null, algorithmId must not be none.");
}
}
return new HMacJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key);
}
@ -248,10 +250,12 @@ public class JWTSignerUtil {
* @return 签名器
*/
public static JWTSigner createSigner(String algorithmId, KeyPair keyPair) {
Assert.notNull(keyPair, "Signer key pair must be not null!");
if (null == algorithmId || NoneJWTSigner.ID_NONE.equals(algorithmId)) {
if (NoneJWTSigner.isNone(algorithmId)) {
if(null == keyPair){
return none();
}else{
throw new JWTException("When key is not null, algorithmId must not be none.");
}
}
// issue3205@Github
@ -270,11 +274,14 @@ public class JWTSignerUtil {
* @return 签名器
*/
public static JWTSigner createSigner(String algorithmId, Key key) {
Assert.notNull(key, "Signer key must be not null!");
if (null == algorithmId || NoneJWTSigner.ID_NONE.equals(algorithmId)) {
return NoneJWTSigner.NONE;
if (NoneJWTSigner.isNone(algorithmId)) {
if(null == key){
return none();
}else{
throw new JWTException("When key is not null, algorithmId must not be none.");
}
}
if (key instanceof PrivateKey || key instanceof PublicKey) {
// issue3205@Github
if(ReUtil.isMatch("ES\\d{3}", algorithmId)){

View File

@ -10,10 +10,27 @@ import cn.hutool.core.util.StrUtil;
*/
public class NoneJWTSigner implements JWTSigner {
/**
* 定义一个常量ID_NONE表示没有ID的情况
*/
public static final String ID_NONE = "none";
/**
* 创建一个NoneJWTSigner实例用于处理没有签名的JWT
*/
public static NoneJWTSigner NONE = new NoneJWTSigner();
/**
* 判断给定的算法是否为无签名的算法
*
* @param alg 算法
* @return 如果是无签名的算法则返回true否则返回false
* @since 5.8.42
*/
public static boolean isNone(final String alg) {
return StrUtil.isBlank( alg) || StrUtil.equalsIgnoreCase(alg, ID_NONE);
}
@Override
public String sign(String headerBase64, String payloadBase64) {
return StrUtil.EMPTY;

View File

@ -19,6 +19,6 @@ public class Issue3732Test {
// 创建 JWT token
String token = JWTUtil.createToken(payload, SIGNER);
assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoidGVzdCJ9.pD3Xz41rtXvU3G1c_yS7ir01FXmDvtjjAOU2HYd8MdA", token);
assertEquals("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoidGVzdCJ9.eS1hjkb2ympf7Gtnh_Xmzmb29bXt3J-1SyNTLMBipbY", token);
}
}

View File

@ -0,0 +1,67 @@
package cn.hutool.jwt;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
public class Issue4105Test {
@Test
void verifyNoneTest() {
// {"alg": "none"}.{"exp": 1642196407}
// 当定义alg为none时校验总是成功
String head = Base64.encode("{\"alg\": \"none\"}");
String payload = Base64.encode("{\"exp\": 1642196407}");
String token = StrUtil.format("{}.{}.", head, payload);
final JWT jwt = JWTUtil.parseToken(token);
Assertions.assertNull(jwt.getSigner());
// 对于签名为none的JWTverify()方法总是返回true
Assertions.assertTrue(jwt.verify());
// 对于签名为none的JWT但是定义了key不一致报错
final JWT jwt2 = JWTUtil.parseToken(token);
Assertions.assertThrows(JWTException.class, ()-> jwt2.setKey("123".getBytes(StandardCharsets.UTF_8)).verify());
}
@Test
void verifyEmptyTest() {
// {"alg": "none"}.{"exp": 1642196407}
// 当定义alg为none时校验总是成功
String head = Base64.encode("{\"alg\": \"\"}");
String payload = Base64.encode("{\"exp\": 1642196407}");
String token = StrUtil.format("{}.{}.", head, payload);
final JWT jwt = JWTUtil.parseToken(token);
Assertions.assertNull(jwt.getSigner());
// 对于签名为none的JWTverify()方法总是返回true
Assertions.assertTrue(jwt.verify());
// 对于签名为none的JWT但是定义了key不一致报错
final JWT jwt2 = JWTUtil.parseToken(token);
Assertions.assertThrows(JWTException.class, ()-> jwt2.setKey("123".getBytes(StandardCharsets.UTF_8)).verify());
}
@Test
void verifyHs256Test() {
// {"alg": "none"}.{"exp": 1642196407}
// 当定义alg为none时校验总是成功
String head = Base64.encode("{\"alg\": \"HS256\"}");
String payload = Base64.encode("{\"exp\": 1642196407}");
String token = StrUtil.format("{}.{}.", head, payload);
final JWT jwt = JWTUtil.parseToken(token);
Assertions.assertNull(jwt.getSigner());
// 未定义签名器或key但是JWT中要求了签名算法异常
Assertions.assertThrows(JWTException.class, jwt::verify);
// 手动定义签名器但是签名部分为空或不一致返回false
final JWT jwt2 = JWTUtil.parseToken(token);
Assertions.assertFalse(jwt2.setKey("123".getBytes(StandardCharsets.UTF_8)).verify());
}
}

View File

@ -20,7 +20,7 @@ public class IssueI6IS5BTest {
final JwtToken jwtToken = new JwtToken();
jwtToken.setIat(iat);
final String token = JWTUtil.createToken(JSONUtil.parseObj(jwtToken), "123".getBytes(StandardCharsets.UTF_8));
assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Nzc3NzI4MDB9.SXU_mm1wT5lNoK-Dq5Y8f3BItv_44zuAlyeWLqajpXg", token);
assertEquals("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2Nzc3NzI4MDB9.W88PB2ovAqCXV4QdbeKbdFW-P057xOTXEosD8hbOa9U", token);
final JSONObject payloads = JWTUtil.parseToken(token).getPayloads();
assertEquals("{\"iat\":1677772800}", payloads.toString());
final JwtToken o = payloads.toBean(JwtToken.class);
@ -38,7 +38,7 @@ public class IssueI6IS5BTest {
final JwtToken2 jwtToken = new JwtToken2();
jwtToken.setIat(iat);
final String token = JWTUtil.createToken(JSONUtil.parseObj(jwtToken), "123".getBytes(StandardCharsets.UTF_8));
assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Nzc3NzI4MDB9.SXU_mm1wT5lNoK-Dq5Y8f3BItv_44zuAlyeWLqajpXg", token);
assertEquals("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2Nzc3NzI4MDB9.W88PB2ovAqCXV4QdbeKbdFW-P057xOTXEosD8hbOa9U", token);
final JSONObject payloads = JWTUtil.parseToken(token).getPayloads();
assertEquals("{\"iat\":1677772800}", payloads.toString());
final JwtToken2 o = payloads.toBean(JwtToken2.class);

View File

@ -19,9 +19,9 @@ public class JWTTest {
.setExpiresAt(DateUtil.parse("2022-01-01"))
.setKey(key);
final String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
final String rightToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imxvb2x5IiwiYWRtaW4iOnRydWUsImV4cCI6MTY0MDk2NjQwMH0." +
"bXlSnqVeJXWqUIt7HyEhgKNVlIPjkumHlAwFY-5YCtk";
"8siIwEMHf-DRyUjVElS_yipb6Mo3c1z0wFiheGXWGQw";
final String token = jwt.sign();
assertEquals(rightToken, token);
@ -58,7 +58,7 @@ public class JWTTest {
.setPayload("admin", true)
.setSigner(JWTSignerUtil.none());
final String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0." +
final String rightToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imxvb2x5IiwiYWRtaW4iOnRydWV9.";
final String token = jwt.sign();

View File

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

View File

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

View File

@ -79,7 +79,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
/**
* 自定义需要处理的sheet编号如果-1表示处理所有sheet
*/
private int rid = -1;
private int sheetIndex = -1;
/**
* sheet名称主要用于使用sheet名读取的情况
*/
@ -132,7 +132,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
* @throws POIException IO异常包装
*/
public Excel03SaxReader read(POIFSFileSystem fs, String idOrRidOrSheetName) throws POIException {
this.rid = getSheetIndex(idOrRidOrSheetName);
this.sheetIndex = getSheetIndex(idOrRidOrSheetName);
formatListener = new FormatTrackingHSSFListener(new MissingRecordAwareHSSFListener(this));
final HSSFRequest request = new HSSFRequest();
@ -162,7 +162,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
* @return sheet序号
*/
public int getSheetIndex() {
return this.rid;
return this.sheetIndex;
}
/**
@ -175,8 +175,8 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
return this.sheetName;
}
if (this.boundSheetRecords.size() > this.rid) {
return this.boundSheetRecords.get(this.rid > -1 ? this.rid : this.curRid).getSheetname();
if (this.boundSheetRecords.size() > this.sheetIndex) {
return this.boundSheetRecords.get(this.sheetIndex > -1 ? this.sheetIndex : this.curRid).getSheetname();
}
return null;
@ -189,7 +189,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
*/
@Override
public void processRecord(Record record) {
if (this.rid > -1 && this.curRid > this.rid) {
if (this.sheetIndex > -1 && this.curRid > this.sheetIndex) {
// 指定Sheet之后的数据不再处理
return;
}
@ -200,7 +200,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
boundSheetRecords.add(boundSheetRecord);
final String currentSheetName = boundSheetRecord.getSheetname();
if(null != this.sheetName && StrUtil.equals(this.sheetName, currentSheetName)){
this.rid = this.boundSheetRecords.size() -1;
this.sheetIndex = this.boundSheetRecords.size() -1;
}
} else if (record instanceof SSTRecord) {
// 静态字符串表
@ -215,7 +215,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
curRid++;
}
} else if (record instanceof EOFRecord){
if(this.rid < 0 && null != this.sheetName){
if(this.sheetIndex < 0 && null != this.sheetName){
throw new POIException("Sheet [{}] not exist!", this.sheetName);
}
if(this.curRid != -1 && isProcessCurrentSheet()) {
@ -369,35 +369,35 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
*/
private boolean isProcessCurrentSheet() {
// rid < 0 sheet名称存在说明没有匹配到sheet名称
return (this.rid < 0 && null == this.sheetName) || this.rid == this.curRid;
return (this.sheetIndex < 0 && null == this.sheetName) || this.sheetIndex == this.curRid;
}
/**
* 获取sheet索引从0开始
* <ul>
* <li>传入'rId'开头直接去除rId前缀</li>
* <li>传入纯数字表示sheetIndex直接转换为rid</li>
* <li>传入纯数字表示sheetIndex直接使用</li>
* </ul>
*
* @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称从0开始rid必须加rId前缀例如rId0如果为-1处理所有编号的sheet
* @param sheetIndexOrSheetName Excel中的sheet 编号或sheet名称从0开始如果为-1处理所有编号的sheet
* @return sheet索引从0开始
* @since 5.5.5
*/
private int getSheetIndex(String idOrRidOrSheetName) {
Assert.notBlank(idOrRidOrSheetName, "id or rid or sheetName must be not blank!");
private int getSheetIndex(String sheetIndexOrSheetName) {
Assert.notBlank(sheetIndexOrSheetName, "id or rid or sheetName must be not blank!");
// rid直接处理
if (StrUtil.startWithIgnoreCase(idOrRidOrSheetName, RID_PREFIX)) {
return Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, RID_PREFIX));
} else if(StrUtil.startWithIgnoreCase(idOrRidOrSheetName, SHEET_NAME_PREFIX)){
if (StrUtil.startWithIgnoreCase(sheetIndexOrSheetName, RID_PREFIX)) {
return Integer.parseInt(StrUtil.removePrefixIgnoreCase(sheetIndexOrSheetName, RID_PREFIX));
} else if(StrUtil.startWithIgnoreCase(sheetIndexOrSheetName, SHEET_NAME_PREFIX)){
// since 5.7.10支持任意名称
this.sheetName = StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, SHEET_NAME_PREFIX);
this.sheetName = StrUtil.removePrefixIgnoreCase(sheetIndexOrSheetName, SHEET_NAME_PREFIX);
} else {
try {
return Integer.parseInt(idOrRidOrSheetName);
return Integer.parseInt(sheetIndexOrSheetName);
} catch (NumberFormatException ignore) {
// 如果用于传入非数字按照sheet名称对待
this.sheetName = idOrRidOrSheetName;
this.sheetName = sheetIndexOrSheetName;
}
}

View File

@ -13,12 +13,8 @@ import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import java.awt.Font;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.awt.*;
import java.io.*;
/**
* Word docx生成器
@ -102,7 +98,19 @@ public class Word07Writer implements Closeable {
* @return this
*/
public Word07Writer addText(Font font, String... texts) {
return addText(null, font, texts);
return addText(null, font, null, texts);
}
/**
* 增加一个段落
*
* @param font 字体信息{@link Font}
* @param color 字体颜色{@link Color}
* @param texts 段落中的文本支持多个文本作为一个段落
* @return this
*/
public Word07Writer addText(Font font, Color color, String... texts) {
return addText(null, font, color, texts);
}
/**
@ -114,6 +122,20 @@ public class Word07Writer implements Closeable {
* @return this
*/
public Word07Writer addText(ParagraphAlignment align, Font font, String... texts) {
return addText(align, font, null, texts);
}
/**
* 增加一个段落
*
* @param align 段落对齐方式{@link ParagraphAlignment}
* @param font 字体信息{@link Font}
* @param color 字体颜色{@link Color}
* @param texts 段落中的文本支持多个文本作为一个段落
* @return this
* @since 5.8.42
*/
public Word07Writer addText(ParagraphAlignment align, Font font, Color color, String... texts) {
final XWPFParagraph p = this.doc.createParagraph();
if (null != align) {
p.setAlignment(align);
@ -129,6 +151,11 @@ public class Word07Writer implements Closeable {
run.setBold(font.isBold());
run.setItalic(font.isItalic());
}
if (null != color) {
// setColor expects a pure RGB hex string (no alpha channel)
String hexColor = String.format("%06X", color.getRGB() & 0xFFFFFF);
run.setColor(hexColor);
}
}
}
return this;

View File

@ -0,0 +1,66 @@
package cn.hutool.poi.excel;
import cn.hutool.poi.excel.style.StyleUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
public class Issue4146Test {
@Test
@Disabled
public void writeSheetWithStyleTest() {
ExcelWriter writer = ExcelUtil.getWriter("d:\\test\\issue4146.xlsx", "表格1");
List<TestUser> list = new ArrayList<>();
TestUser test = new TestUser("张三", 18, 90.0, 0.9878);
list.add(test);
test = new TestUser("李四", 18, 79.5, 0.8311);
list.add(test);
test = new TestUser("王五", 18, 89.9, 0.6932);
list.add(test);
test = new TestUser("赵六", 18, 69.9, 0.7912);
list.add(test);
test = new TestUser("孙七", 18, 79.9, 0.6432);
list.add(test);
writer.addHeaderAlias("name", "姓名");
writer.addHeaderAlias("age", "年龄");
writer.addHeaderAlias("score", "分数");
writer.addHeaderAlias("zb", "占比");
writer.setOnlyAlias(true);
writer.write(list, true);
// 百分比的单元格样式必须单独创建使用StyleSet中的样式修改则会修改全局样式
CellStyle percentCellStyle = writer.createCellStyle();
percentCellStyle.setDataFormat(writer.getWorkbook().createDataFormat().getFormat("0.00%"));
// 填充背景颜色必须指定FillPatternType才有效
StyleUtil.setColor(percentCellStyle, IndexedColors.YELLOW, FillPatternType.SOLID_FOREGROUND);
// 设置边框颜色和粗细
StyleUtil.setBorder(percentCellStyle, BorderStyle.THIN, IndexedColors.BLACK);
final int rowCount = writer.getRowCount();
// 设置列样式无效除非将默认样式清除因此必须在写出数据后为单元格指定自定义的样式
for (int i = 1; i < rowCount; i++) {
writer.setStyle(percentCellStyle, 3, i);
}
writer.close();
}
@Data
@AllArgsConstructor
static class TestUser {
private String name;
private Integer age;
private Double score;
private Double zb;
}
}

View File

@ -5,16 +5,20 @@ import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Console;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.awt.Font;
import java.awt.*;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class WordWriterTest {
@Test
@ -23,6 +27,7 @@ public class WordWriterTest {
Word07Writer writer = new Word07Writer();
writer.addText(new Font("方正小标宋简体", Font.PLAIN, 22), "我是第一部分", "我是第二部分");
writer.addText(new Font("宋体", Font.PLAIN, 22), "我是正文第一部分", "我是正文第二部分");
writer.addText(new Font("宋体", Font.PLAIN, 22), Color.RED, "我是正文第三部分", "我是正文第四部分");
writer.flush(FileUtil.file("e:/wordWrite.docx"));
writer.close();
Console.log("OK");
@ -95,4 +100,17 @@ public class WordWriterTest {
word07Writer.addTable(list);
word07Writer.close();
}
@Test
public void addTextShouldStripAlphaAndUseRgbHex() {
final Word07Writer writer = new Word07Writer();
final Color colorWithAlpha = new Color(0x12, 0x34, 0x56, 0x7F);
writer.addText(new Font("宋体", Font.PLAIN, 12), colorWithAlpha, "带颜色的段落");
final XWPFParagraph paragraph = writer.getDoc().getParagraphArray(0);
final XWPFRun run = paragraph.getRuns().get(0);
assertEquals("123456", run.getColor());
writer.close();
}
}

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More