mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-12-06 09:09:10 +08:00
Compare commits
89 Commits
fa308b5ef0
...
4656e8d3ec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4656e8d3ec | ||
|
|
3a96690070 | ||
|
|
f4e5a10697 | ||
|
|
6802979215 | ||
|
|
662cb9aad1 | ||
|
|
6bb750fb2c | ||
|
|
95c8e73c16 | ||
|
|
af97ba4084 | ||
|
|
8afee460fd | ||
|
|
0f60aa021a | ||
|
|
d821c410c6 | ||
|
|
882f3923ce | ||
|
|
1d9b0ebbd2 | ||
|
|
9191e49c21 | ||
|
|
a56d2c03dc | ||
|
|
d9cbb19460 | ||
|
|
f18a2b512f | ||
|
|
6fcd1a2603 | ||
|
|
5ae493119f | ||
|
|
ea717843f6 | ||
|
|
b8908cf3ef | ||
|
|
ab1774d4e6 | ||
|
|
ccaecf6bc9 | ||
|
|
3d2dd38add | ||
|
|
d64a0d36aa | ||
|
|
ab936327a2 | ||
|
|
6ef5f1c1fd | ||
|
|
6776ddb29d | ||
|
|
449df10509 | ||
|
|
fc5e1ecff9 | ||
|
|
472b0d2841 | ||
|
|
00748130ef | ||
|
|
820e0d0c8b | ||
|
|
86c3c530b6 | ||
|
|
2a08f40527 | ||
|
|
a8cc614fa6 | ||
|
|
a6151b6ee8 | ||
|
|
3c067f1871 | ||
|
|
654db66ecb | ||
|
|
bd42686a65 | ||
|
|
e8eec73125 | ||
|
|
04314cdd4a | ||
|
|
4d04d5daf9 | ||
|
|
1a7d522dff | ||
|
|
eec6056876 | ||
|
|
c6cbaeabff | ||
|
|
9dad6fc4d5 | ||
|
|
5e1110426b | ||
|
|
edb4401e47 | ||
|
|
7258a5b946 | ||
|
|
17f78f8cd4 | ||
|
|
ed27c637b2 | ||
|
|
ff32ed0872 | ||
|
|
9f6d3b5430 | ||
|
|
f9eb29fa87 | ||
|
|
8003c4416a | ||
|
|
c84ec20f5a | ||
|
|
b0c3350ef7 | ||
|
|
44a2afd588 | ||
|
|
41307a6e3d | ||
|
|
9b3414b397 | ||
|
|
f60f20243d | ||
|
|
3b15ae08ae | ||
|
|
62f04b2c0d | ||
|
|
cbade4e239 | ||
|
|
edeb87c7f4 | ||
|
|
a3e58451fc | ||
|
|
02988f3714 | ||
|
|
0cbc36e225 | ||
|
|
ce3d69dd35 | ||
|
|
4162c519b7 | ||
|
|
b136d81720 | ||
|
|
8469fd0c49 | ||
|
|
e9c4e65f97 | ||
|
|
4c563da8bd | ||
|
|
fb95caa7b9 | ||
|
|
25839055d6 | ||
|
|
afc1036fb6 | ||
|
|
7d84d1a81c | ||
|
|
5a3ad06601 | ||
|
|
47c48b23b3 | ||
|
|
dcdba8314d | ||
|
|
97e56c48eb | ||
|
|
ddd3eb34f6 | ||
|
|
4a88a565bf | ||
|
|
9d0b6d652f | ||
|
|
fa238bc4c9 | ||
|
|
29902b9093 | ||
|
|
da2d0823b9 |
39
CHANGELOG.md
39
CHANGELOG.md
@ -1,5 +1,44 @@
|
|||||||
|
|
||||||
# 🚀Changelog
|
# 🚀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改为CharSequence(pr#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)
|
# 5.8.41(2025-10-12)
|
||||||
|
|||||||
12
README-EN.md
12
README-EN.md
@ -18,8 +18,8 @@
|
|||||||
<a target="_blank" href="https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html">
|
<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" />
|
<img src="https://img.shields.io/badge/JDK-8+-green.svg" />
|
||||||
</a>
|
</a>
|
||||||
<a target="_blank" href="https://travis-ci.com/chinabugotech/hutool">
|
<a target="_blank" href="https://app.travis-ci.com/chinabugotech/hutool">
|
||||||
<img src="https://travis-ci.com/chinabugotech/hutool.svg?branch=v5-master" />
|
<img src="https://api.travis-ci.com/chinabugotech/hutool.svg?branch=v5-master" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.codacy.com/gh/chinabugotech/hutool/dashboard?utm_source=github.com&utm_medium=referral&utm_content=chinabugotech/hutool&utm_campaign=Badge_Grade">
|
<a href="https://www.codacy.com/gh/chinabugotech/hutool/dashboard?utm_source=github.com&utm_medium=referral&utm_content=chinabugotech/hutool&utm_campaign=Badge_Grade">
|
||||||
<img src="https://app.codacy.com/project/badge/Grade/8a6897d9de7440dd9de8804c28d2871d"/>
|
<img src="https://app.codacy.com/project/badge/Grade/8a6897d9de7440dd9de8804c28d2871d"/>
|
||||||
@ -49,8 +49,6 @@
|
|||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[**🌎中文说明**](README.md)
|
[**🌎中文说明**](README.md)
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
@ -134,18 +132,18 @@ Each module can be introduced individually, or all modules can be introduced by
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-all</artifactId>
|
<artifactId>hutool-all</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🍐Gradle
|
### 🍐Gradle
|
||||||
```
|
```
|
||||||
implementation 'cn.hutool:hutool-all:5.8.41'
|
implementation 'cn.hutool:hutool-all:5.8.42'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📥Download
|
## 📥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:
|
> 🔔️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.
|
> Hutool 5.x supports JDK8+ and is not tested on Android platforms, and cannot guarantee that all tool classes or tool methods are available.
|
||||||
|
|||||||
12
README.md
12
README.md
@ -18,8 +18,8 @@
|
|||||||
<a target="_blank" href="https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html">
|
<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" />
|
<img src="https://img.shields.io/badge/JDK-8+-green.svg" />
|
||||||
</a>
|
</a>
|
||||||
<a target="_blank" href="https://travis-ci.com/chinabugotech/hutool">
|
<a target="_blank" href="https://app.travis-ci.com/chinabugotech/hutool">
|
||||||
<img src="https://travis-ci.com/chinabugotech/hutool.svg?branch=v5-master" />
|
<img src="https://api.travis-ci.com/chinabugotech/hutool.svg?branch=v5-master" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.codacy.com/gh/chinabugotech/hutool/dashboard?utm_source=github.com&utm_medium=referral&utm_content=chinabugotech/hutool&utm_campaign=Badge_Grade">
|
<a href="https://www.codacy.com/gh/chinabugotech/hutool/dashboard?utm_source=github.com&utm_medium=referral&utm_content=chinabugotech/hutool&utm_campaign=Badge_Grade">
|
||||||
<img src="https://app.codacy.com/project/badge/Grade/8a6897d9de7440dd9de8804c28d2871d"/>
|
<img src="https://app.codacy.com/project/badge/Grade/8a6897d9de7440dd9de8804c28d2871d"/>
|
||||||
@ -46,8 +46,6 @@
|
|||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
=======
|
|
||||||
|
|
||||||
[**🌎English Documentation**](README-EN.md)
|
[**🌎English Documentation**](README-EN.md)
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
@ -124,20 +122,20 @@ Hutool = Hu + tool,是原公司项目底层代码剥离后的开源库,“Hu
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-all</artifactId>
|
<artifactId>hutool-all</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🍐Gradle
|
### 🍐Gradle
|
||||||
```
|
```
|
||||||
implementation 'cn.hutool:hutool-all:5.8.41'
|
implementation 'cn.hutool:hutool-all:5.8.42'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 📥下载jar
|
### 📥下载jar
|
||||||
|
|
||||||
点击以下链接,下载`hutool-all-X.X.X.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平台没有测试,不能保证所有工具类或工具方法可用。
|
> Hutool 5.x支持JDK8+,对Android平台没有测试,不能保证所有工具类或工具方法可用。
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
5.8.41
|
5.8.42
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
var version = '5.8.41'
|
var version = '5.8.42'
|
||||||
@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-ai</artifactId>
|
<artifactId>hutool-ai</artifactId>
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package cn.hutool.ai.core;
|
package cn.hutool.ai.core;
|
||||||
|
|
||||||
|
import java.net.Proxy;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -142,4 +143,36 @@ public interface AIConfig {
|
|||||||
*/
|
*/
|
||||||
int getReadTimeout();
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
package cn.hutool.ai.core;
|
package cn.hutool.ai.core;
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.net.Proxy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于AIConfig的创建,创建同时支持链式设置参数
|
* 用于AIConfig的创建,创建同时支持链式设置参数
|
||||||
@ -160,6 +161,21 @@ public class AIConfigBuilder {
|
|||||||
return this;
|
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实例
|
* 返回config实例
|
||||||
*
|
*
|
||||||
|
|||||||
@ -28,6 +28,8 @@ import java.net.URL;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static cn.hutool.core.thread.GlobalThreadPool.execute;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础AIService,包含基公共参数和公共方法
|
* 基础AIService,包含基公共参数和公共方法
|
||||||
*
|
*
|
||||||
@ -56,11 +58,14 @@ public class BaseAIService {
|
|||||||
//链式构建请求
|
//链式构建请求
|
||||||
try {
|
try {
|
||||||
//设置超时3分钟
|
//设置超时3分钟
|
||||||
return HttpRequest.get(config.getApiUrl() + endpoint)
|
HttpRequest httpRequest = HttpRequest.get(config.getApiUrl() + endpoint)
|
||||||
.header(Header.ACCEPT, "application/json")
|
.header(Header.ACCEPT, "application/json")
|
||||||
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
|
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
|
||||||
.timeout(config.getTimeout())
|
.timeout(config.getTimeout());
|
||||||
.execute();
|
if (config.getHasProxy()) {
|
||||||
|
httpRequest.setProxy(config.getProxy());
|
||||||
|
}
|
||||||
|
return httpRequest.execute();
|
||||||
} catch (final AIException e) {
|
} catch (final AIException e) {
|
||||||
throw new AIException("Failed to send GET request: " + e.getMessage(), 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) {
|
protected HttpResponse sendPost(final String endpoint, final String paramJson) {
|
||||||
//链式构建请求
|
//链式构建请求
|
||||||
try {
|
try {
|
||||||
return HttpRequest.post(config.getApiUrl() + endpoint)
|
HttpRequest httpRequest = HttpRequest.post(config.getApiUrl() + endpoint)
|
||||||
.header(Header.CONTENT_TYPE, "application/json")
|
.header(Header.CONTENT_TYPE, "application/json")
|
||||||
.header(Header.ACCEPT, "application/json")
|
.header(Header.ACCEPT, "application/json")
|
||||||
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
|
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
|
||||||
.body(paramJson)
|
.body(paramJson)
|
||||||
.timeout(config.getTimeout())
|
.timeout(config.getTimeout());
|
||||||
.execute();
|
if (config.getHasProxy()) {
|
||||||
|
httpRequest.setProxy(config.getProxy());
|
||||||
|
}
|
||||||
|
return httpRequest.execute();
|
||||||
} catch (final AIException e) {
|
} catch (final AIException e) {
|
||||||
throw new AIException("Failed to send POST request:" + e.getMessage(), e);
|
throw new AIException("Failed to send POST request:" + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@ -98,13 +106,16 @@ public class BaseAIService {
|
|||||||
//链式构建请求
|
//链式构建请求
|
||||||
try {
|
try {
|
||||||
//设置超时3分钟
|
//设置超时3分钟
|
||||||
return HttpRequest.post(config.getApiUrl() + endpoint)
|
HttpRequest httpRequest = HttpRequest.post(config.getApiUrl() + endpoint)
|
||||||
.header(Header.CONTENT_TYPE, "multipart/form-data")
|
.header(Header.CONTENT_TYPE, "multipart/form-data")
|
||||||
.header(Header.ACCEPT, "application/json")
|
.header(Header.ACCEPT, "application/json")
|
||||||
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
|
.header(Header.AUTHORIZATION, "Bearer " + config.getApiKey())
|
||||||
.form(paramMap)
|
.form(paramMap)
|
||||||
.timeout(config.getTimeout())
|
.timeout(config.getTimeout());
|
||||||
.execute();
|
if (config.getHasProxy()) {
|
||||||
|
httpRequest.setProxy(config.getProxy());
|
||||||
|
}
|
||||||
|
return httpRequest.execute();
|
||||||
} catch (final AIException e) {
|
} catch (final AIException e) {
|
||||||
throw new AIException("Failed to send POST request:" + e.getMessage(), 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);
|
URL apiUrl = new URL(config.getApiUrl() + endpoint);
|
||||||
connection = (HttpURLConnection) apiUrl.openConnection();
|
connection = (HttpURLConnection) apiUrl.openConnection();
|
||||||
|
if (config.getHasProxy()) {
|
||||||
|
connection = (HttpURLConnection) apiUrl.openConnection(config.getProxy());
|
||||||
|
}
|
||||||
connection.setRequestMethod(Method.POST.name());
|
connection.setRequestMethod(Method.POST.name());
|
||||||
connection.setRequestProperty(Header.CONTENT_TYPE.getValue(), "application/json");
|
connection.setRequestProperty(Header.CONTENT_TYPE.getValue(), "application/json");
|
||||||
connection.setRequestProperty(Header.AUTHORIZATION.getValue(), "Bearer " + config.getApiKey());
|
connection.setRequestProperty(Header.AUTHORIZATION.getValue(), "Bearer " + config.getApiKey());
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package cn.hutool.ai.core;
|
package cn.hutool.ai.core;
|
||||||
|
|
||||||
|
import java.net.Proxy;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@ -39,6 +40,10 @@ public class BaseConfig implements AIConfig {
|
|||||||
protected volatile int timeout = 180000;
|
protected volatile int timeout = 180000;
|
||||||
//读取超时时间
|
//读取超时时间
|
||||||
protected volatile int readTimeout = 300000;
|
protected volatile int readTimeout = 300000;
|
||||||
|
//是否设置代理
|
||||||
|
protected volatile boolean hasProxy = false;
|
||||||
|
//代理设置
|
||||||
|
protected volatile Proxy proxy;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setApiKey(final String apiKey) {
|
public void setApiKey(final String apiKey) {
|
||||||
@ -104,4 +109,24 @@ public class BaseConfig implements AIConfig {
|
|||||||
public void setReadTimeout(final int readTimeout) {
|
public void setReadTimeout(final int readTimeout) {
|
||||||
this.readTimeout = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,6 +35,7 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
@ -101,7 +102,7 @@ class OpenaiServiceTest {
|
|||||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||||
.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);
|
.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);
|
||||||
String prompt = "图片上有些什么?";
|
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作为结束标志
|
||||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||||
@ -200,7 +201,7 @@ class OpenaiServiceTest {
|
|||||||
void moderations() {
|
void moderations() {
|
||||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||||
.setApiKey(key).setModel(Models.Openai.OMNI_MODERATION_LATEST.getModel()).build(), OpenaiService.class);
|
.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);
|
assertNotNull(moderations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-all</artifactId>
|
<artifactId>hutool-all</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-aop</artifactId>
|
<artifactId>hutool-aop</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-bloomFilter</artifactId>
|
<artifactId>hutool-bloomFilter</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-bom</artifactId>
|
<artifactId>hutool-bom</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-cache</artifactId>
|
<artifactId>hutool-cache</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-captcha</artifactId>
|
<artifactId>hutool-captcha</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-core</artifactId>
|
<artifactId>hutool-core</artifactId>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import java.lang.reflect.Method;
|
|||||||
public class CacheableAnnotationAttribute implements AnnotationAttribute {
|
public class CacheableAnnotationAttribute implements AnnotationAttribute {
|
||||||
|
|
||||||
private volatile boolean valueInvoked;
|
private volatile boolean valueInvoked;
|
||||||
private Object value;
|
private volatile Object value;
|
||||||
|
|
||||||
private boolean defaultValueInvoked;
|
private boolean defaultValueInvoked;
|
||||||
private Object defaultValue;
|
private Object defaultValue;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import cn.hutool.core.util.PageUtil;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -721,4 +722,32 @@ public class ListUtil {
|
|||||||
}
|
}
|
||||||
return list;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import java.util.Date;
|
|||||||
* @author looly
|
* @author looly
|
||||||
* @since 5.8.13
|
* @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 static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private final Number number;
|
private final Number number;
|
||||||
@ -86,4 +86,13 @@ public class NumberWithFormat extends Number implements TypeConverter {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return this.number.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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,13 +73,13 @@ public class DateModifier {
|
|||||||
case ROUND:
|
case ROUND:
|
||||||
int min = isAM ? 0 : 12;
|
int min = isAM ? 0 : 12;
|
||||||
int max = isAM ? 11 : 23;
|
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);
|
int value = calendar.get(Calendar.HOUR_OF_DAY);
|
||||||
calendar.set(Calendar.HOUR_OF_DAY, (value < href) ? min : max);
|
calendar.set(Calendar.HOUR_OF_DAY, (value < href) ? min : max);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// 处理下一级别字段
|
// 处理下一级别字段
|
||||||
return modify(calendar, dateField + 1, modifyType);
|
return modify(calendar, dateField + 1, modifyType, truncateMillisecond);
|
||||||
}
|
}
|
||||||
|
|
||||||
final int endField = truncateMillisecond ? Calendar.SECOND : Calendar.MILLISECOND;
|
final int endField = truncateMillisecond ? Calendar.SECOND : Calendar.MILLISECOND;
|
||||||
|
|||||||
@ -1901,6 +1901,8 @@ public class ImgUtil {
|
|||||||
// issue#IAPZG7
|
// issue#IAPZG7
|
||||||
// FileCacheImageOutputStream会产生临时文件,此处关闭清除
|
// FileCacheImageOutputStream会产生临时文件,此处关闭清除
|
||||||
IoUtil.close(output);
|
IoUtil.close(output);
|
||||||
|
// issue#ID6VNJ
|
||||||
|
flush(image);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -240,6 +240,7 @@ public class FileUtil extends PathUtil {
|
|||||||
if (path == null) {
|
if (path == null) {
|
||||||
return new ArrayList<>(0);
|
return new ArrayList<>(0);
|
||||||
}
|
}
|
||||||
|
path = getAbsolutePath(path);
|
||||||
int index = path.lastIndexOf(FileUtil.JAR_PATH_EXT);
|
int index = path.lastIndexOf(FileUtil.JAR_PATH_EXT);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
// 普通目录
|
// 普通目录
|
||||||
@ -253,8 +254,6 @@ public class FileUtil extends PathUtil {
|
|||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
// jar文件
|
|
||||||
path = getAbsolutePath(path);
|
|
||||||
// jar文件中的路径
|
// jar文件中的路径
|
||||||
index = index + FileUtil.JAR_FILE_EXT.length();
|
index = index + FileUtil.JAR_FILE_EXT.length();
|
||||||
JarFile jarFile = null;
|
JarFile jarFile = null;
|
||||||
|
|||||||
@ -238,7 +238,7 @@ public class FileNameUtil {
|
|||||||
// issue#I4W5FS@Gitee
|
// issue#I4W5FS@Gitee
|
||||||
final int secondToLastIndex = fileName.substring(0, index).lastIndexOf(StrUtil.DOT);
|
final int secondToLastIndex = fileName.substring(0, index).lastIndexOf(StrUtil.DOT);
|
||||||
final String substr = fileName.substring(secondToLastIndex == -1 ? index : secondToLastIndex + 1);
|
final String substr = fileName.substring(secondToLastIndex == -1 ? index : secondToLastIndex + 1);
|
||||||
if (StrUtil.containsAny(substr, SPECIAL_SUFFIX)) {
|
if (StrUtil.equalsAny(substr, SPECIAL_SUFFIX)) {
|
||||||
return substr;
|
return substr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import cn.hutool.core.util.ReUtil;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.core.util.IdcardUtil;
|
import cn.hutool.core.util.IdcardUtil;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@ -1123,8 +1124,11 @@ public class Validator {
|
|||||||
Assert.notNull(value);
|
Assert.notNull(value);
|
||||||
Assert.notNull(min);
|
Assert.notNull(min);
|
||||||
Assert.notNull(max);
|
Assert.notNull(max);
|
||||||
final double doubleValue = value.doubleValue();
|
// 通过 NumberUtil 转换为 BigDecimal,使用 BigDecimal 进行比较以保留精度
|
||||||
return (doubleValue >= min.doubleValue()) && (doubleValue <= max.doubleValue());
|
BigDecimal valBd = NumberUtil.toBigDecimal(value);
|
||||||
|
BigDecimal minBd = NumberUtil.toBigDecimal(min);
|
||||||
|
BigDecimal maxBd = NumberUtil.toBigDecimal(max);
|
||||||
|
return valBd.compareTo(minBd) >= 0 && valBd.compareTo(maxBd) <= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -168,7 +168,7 @@ public class MutableLong extends Number implements Comparable<MutableLong>, Muta
|
|||||||
* </ol>
|
* </ol>
|
||||||
*
|
*
|
||||||
* @param obj 比对的对象
|
* @param obj 比对的对象
|
||||||
* @return 相同返回<code>true</code>,否则 {@code false}
|
* @return 相同返回{@code true},否则 {@code false}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(final Object obj) {
|
public boolean equals(final Object obj) {
|
||||||
|
|||||||
@ -39,7 +39,7 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
|
|||||||
}
|
}
|
||||||
this.inverse.put(value, key);
|
this.inverse.put(value, key);
|
||||||
}
|
}
|
||||||
return super.put(key, value);
|
return oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -94,10 +94,12 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public V putIfAbsent(K key, V value) {
|
public V putIfAbsent(K key, V value) {
|
||||||
if (null != this.inverse) {
|
final V oldValue = super.putIfAbsent(key, value);
|
||||||
this.inverse.putIfAbsent(value, key);
|
// 只有当oldValue为null时(即key之前不存在),才更新反向Map
|
||||||
|
if (null == oldValue && null != this.inverse) {
|
||||||
|
this.inverse.put(value, key);
|
||||||
}
|
}
|
||||||
return super.putIfAbsent(key, value);
|
return oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
package cn.hutool.core.math;
|
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.ArrayUtil;
|
||||||
import cn.hutool.core.util.NumberUtil;
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 排列A(n, m)<br>
|
* 排列A(n, m)<br>
|
||||||
@ -47,10 +44,23 @@ public class Arrangement implements Serializable {
|
|||||||
* @return 排列数
|
* @return 排列数
|
||||||
*/
|
*/
|
||||||
public static long count(int n, int m) {
|
public static long count(int n, int m) {
|
||||||
if (n == m) {
|
if (m < 0 || m > n) {
|
||||||
return NumberUtil.factorial(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 选择个数
|
* <p>
|
||||||
* @return 所有排列列表
|
* 说明:
|
||||||
|
* <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) {
|
public List<String[]> select(int m) {
|
||||||
final List<String[]> result = new ArrayList<>((int) count(this.datas.length, m));
|
if (m < 0 || m > datas.length) {
|
||||||
select(this.datas, new String[m], 0, result);
|
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;
|
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() {
|
public List<String[]> selectAll() {
|
||||||
final List<String[]> result = new ArrayList<>((int) countAll(this.datas.length));
|
List<String[]> result = new ArrayList<>();
|
||||||
for (int i = 1; i <= this.datas.length; i++) {
|
for (int m = 1; m <= datas.length; m++) {
|
||||||
result.addAll(select(i));
|
result.addAll(select(m));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -124,4 +193,192 @@ public class Arrangement implements Serializable {
|
|||||||
select(ArrayUtil.remove(datas, i), resultList, resultIndex + 1, result);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
package cn.hutool.core.math;
|
package cn.hutool.core.math;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import cn.hutool.core.util.NumberUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组合,即C(n, m)<br>
|
* 组合,即C(n, m)<br>
|
||||||
* 排列组合相关类 参考:http://cgs1999.iteye.com/blog/2327664
|
* 排列组合相关类 参考:http://cgs1999.iteye.com/blog/2327664
|
||||||
@ -32,18 +32,72 @@ public class Combination implements Serializable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算组合数,即C(n, m) = n!/((n-m)! * m!)
|
* 计算组合数,即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 n 总数
|
||||||
* @param m 选择的个数
|
* @param m 选择的个数
|
||||||
* @return 组合数
|
* @return 组合数
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public static long count(int n, int m) {
|
public static long count(int n, int m) {
|
||||||
if (0 == m || n == m) {
|
BigInteger big = countBig(n, m);
|
||||||
return 1;
|
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)...
|
* 计算组合总数,即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);
|
select(i + 1, resultList, resultIndex + 1, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,15 +4,35 @@ import java.io.Serializable;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 本地端口生成器<br>
|
* 本地端口生成器(LocalPortGenerator)。
|
||||||
* 用于生成本地可用(未被占用)的端口号<br>
|
* <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
|
* @author looly
|
||||||
* @since 4.0.3
|
* @since 4.0.3
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class LocalPortGenerater implements Serializable{
|
|
||||||
|
|
||||||
|
public class LocalPortGenerater implements Serializable{
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
/** 备选的本地端口 */
|
/** 备选的本地端口 */
|
||||||
|
|||||||
@ -28,6 +28,9 @@ import java.util.function.Predicate;
|
|||||||
*/
|
*/
|
||||||
public class CharSequenceUtil {
|
public class CharSequenceUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引值:{@code -1}
|
||||||
|
*/
|
||||||
public static final int INDEX_NOT_FOUND = Finder.INDEX_NOT_FOUND;
|
public static final int INDEX_NOT_FOUND = Finder.INDEX_NOT_FOUND;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -707,7 +710,7 @@ public class CharSequenceUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isStartWith = str.toString()
|
boolean isStartWith = str.toString()
|
||||||
.regionMatches(ignoreCase, 0, prefix.toString(), 0, prefix.length());
|
.regionMatches(ignoreCase, 0, prefix.toString(), 0, prefix.length());
|
||||||
|
|
||||||
if (isStartWith) {
|
if (isStartWith) {
|
||||||
return (false == ignoreEquals) || (false == equals(str, prefix, ignoreCase));
|
return (false == ignoreEquals) || (false == equals(str, prefix, ignoreCase));
|
||||||
@ -842,7 +845,7 @@ public class CharSequenceUtil {
|
|||||||
|
|
||||||
final int strOffset = str.length() - suffix.length();
|
final int strOffset = str.length() - suffix.length();
|
||||||
boolean isEndWith = str.toString()
|
boolean isEndWith = str.toString()
|
||||||
.regionMatches(ignoreCase, strOffset, suffix.toString(), 0, suffix.length());
|
.regionMatches(ignoreCase, strOffset, suffix.toString(), 0, suffix.length());
|
||||||
|
|
||||||
if (isEndWith) {
|
if (isEndWith) {
|
||||||
return (false == ignoreEquals) || (false == equals(str, suffix, ignoreCase));
|
return (false == ignoreEquals) || (false == equals(str, suffix, ignoreCase));
|
||||||
@ -1280,7 +1283,7 @@ public class CharSequenceUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new StrFinder(searchStr, ignoreCase)
|
return new StrFinder(searchStr, ignoreCase)
|
||||||
.setText(text).setNegative(true).start(from);
|
.setText(text).setNegative(true).start(from);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1557,7 +1560,7 @@ public class CharSequenceUtil {
|
|||||||
|
|
||||||
final String str2 = str.toString();
|
final String str2 = str.toString();
|
||||||
int toIndex = str2.length();
|
int toIndex = str2.length();
|
||||||
while (str2.startsWith(suffixStr, toIndex - suffixLength)){
|
while (str2.startsWith(suffixStr, toIndex - suffixLength)) {
|
||||||
toIndex -= suffixLength;
|
toIndex -= suffixLength;
|
||||||
}
|
}
|
||||||
return subPre(str2, toIndex);
|
return subPre(str2, toIndex);
|
||||||
@ -1689,9 +1692,9 @@ public class CharSequenceUtil {
|
|||||||
* }
|
* }
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* @param str 被处理的字符串
|
* @param str 被处理的字符串
|
||||||
* @param prefix 前缀
|
* @param prefix 前缀
|
||||||
* @param suffix 后缀
|
* @param suffix 后缀
|
||||||
* @param ignoreCase 是否忽略大小写
|
* @param ignoreCase 是否忽略大小写
|
||||||
* @return 处理后的字符串
|
* @return 处理后的字符串
|
||||||
* @since 3.1.2
|
* @since 3.1.2
|
||||||
@ -1707,17 +1710,17 @@ public class CharSequenceUtil {
|
|||||||
|
|
||||||
if (startWith(str2, prefix, ignoreCase)) {
|
if (startWith(str2, prefix, ignoreCase)) {
|
||||||
from = prefix.length();
|
from = prefix.length();
|
||||||
if(from == to){
|
if (from == to) {
|
||||||
// "a", "a", "a" -> ""
|
// "a", "a", "a" -> ""
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (endWith(str2, suffix, ignoreCase)) {
|
if (endWith(str2, suffix, ignoreCase)) {
|
||||||
to -= suffix.length();
|
to -= suffix.length();
|
||||||
if(from == to){
|
if (from == to) {
|
||||||
// "a", "a", "a" -> ""
|
// "a", "a", "a" -> ""
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
} else if(to < from){
|
} else if (to < from) {
|
||||||
// pre去除后和suffix有重叠,如 ("aba", "ab", "ba") -> "a"
|
// pre去除后和suffix有重叠,如 ("aba", "ab", "ba") -> "a"
|
||||||
to += suffix.length();
|
to += suffix.length();
|
||||||
}
|
}
|
||||||
@ -1787,22 +1790,22 @@ public class CharSequenceUtil {
|
|||||||
int from = 0;
|
int from = 0;
|
||||||
int to = str2.length();
|
int to = str2.length();
|
||||||
|
|
||||||
if(!prefixStr.isEmpty()){
|
if (!prefixStr.isEmpty()) {
|
||||||
while (str2.startsWith(prefixStr, from)) {
|
while (str2.startsWith(prefixStr, from)) {
|
||||||
from += prefix.length();
|
from += prefix.length();
|
||||||
if(from == to){
|
if (from == to) {
|
||||||
// "a", "a", "a" -> ""
|
// "a", "a", "a" -> ""
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!suffixStr.isEmpty()){
|
if (!suffixStr.isEmpty()) {
|
||||||
while (str2.startsWith(suffixStr, to - suffixStr.length())) {
|
while (str2.startsWith(suffixStr, to - suffixStr.length())) {
|
||||||
to -= suffixStr.length();
|
to -= suffixStr.length();
|
||||||
if(from == to){
|
if (from == to) {
|
||||||
// "a", "a", "a" -> ""
|
// "a", "a", "a" -> ""
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}else if(to < from){
|
} else if (to < from) {
|
||||||
// pre去除后和suffix有重叠,如 ("aba", "ab", "ba") -> "a"
|
// pre去除后和suffix有重叠,如 ("aba", "ab", "ba") -> "a"
|
||||||
to += suffixStr.length();
|
to += suffixStr.length();
|
||||||
break;
|
break;
|
||||||
@ -2573,8 +2576,8 @@ public class CharSequenceUtil {
|
|||||||
*/
|
*/
|
||||||
public static String[] subBetweenAll(CharSequence str, CharSequence prefix, CharSequence suffix) {
|
public static String[] subBetweenAll(CharSequence str, CharSequence prefix, CharSequence suffix) {
|
||||||
if (hasEmpty(str, prefix, suffix) ||
|
if (hasEmpty(str, prefix, suffix) ||
|
||||||
// 不包含起始字符串,则肯定没有子串
|
// 不包含起始字符串,则肯定没有子串
|
||||||
false == contains(str, prefix)) {
|
false == contains(str, prefix)) {
|
||||||
return new String[0];
|
return new String[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4263,7 +4266,7 @@ public class CharSequenceUtil {
|
|||||||
if (null == str) {
|
if (null == str) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if(0 == str.length()){
|
if (0 == str.length()) {
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
return str.toString().toLowerCase();
|
return str.toString().toLowerCase();
|
||||||
@ -4281,7 +4284,7 @@ public class CharSequenceUtil {
|
|||||||
if (null == str) {
|
if (null == str) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if(0 == str.length()){
|
if (0 == str.length()) {
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
return str.toString().toUpperCase();
|
return str.toString().toUpperCase();
|
||||||
@ -4559,9 +4562,21 @@ public class CharSequenceUtil {
|
|||||||
* @return StringBuilder对象
|
* @return StringBuilder对象
|
||||||
*/
|
*/
|
||||||
public static StringBuilder builder(CharSequence... strs) {
|
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();
|
final StringBuilder sb = new StringBuilder();
|
||||||
for (CharSequence str : strs) {
|
for (final CharSequence str : strs) {
|
||||||
sb.append(str);
|
sb.append(strEditor.apply(str));
|
||||||
}
|
}
|
||||||
return sb;
|
return sb;
|
||||||
}
|
}
|
||||||
@ -4686,8 +4701,8 @@ public class CharSequenceUtil {
|
|||||||
final int preLength = suffixLength + (maxLength - 3) % 2; // suffixLength 或 suffixLength + 1
|
final int preLength = suffixLength + (maxLength - 3) % 2; // suffixLength 或 suffixLength + 1
|
||||||
final String str2 = str.toString();
|
final String str2 = str.toString();
|
||||||
return format("{}...{}",
|
return format("{}...{}",
|
||||||
str2.substring(0, preLength),
|
str2.substring(0, preLength),
|
||||||
str2.substring(strLength - suffixLength));
|
str2.substring(strLength - suffixLength));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4772,15 +4787,15 @@ public class CharSequenceUtil {
|
|||||||
if (moveLength > 0) {
|
if (moveLength > 0) {
|
||||||
int endAfterMove = Math.min(endExclude + moveLength, str.length());
|
int endAfterMove = Math.min(endExclude + moveLength, str.length());
|
||||||
strBuilder.append(str.subSequence(0, startInclude))//
|
strBuilder.append(str.subSequence(0, startInclude))//
|
||||||
.append(str.subSequence(endExclude, endAfterMove))//
|
.append(str.subSequence(endExclude, endAfterMove))//
|
||||||
.append(str.subSequence(startInclude, endExclude))//
|
.append(str.subSequence(startInclude, endExclude))//
|
||||||
.append(str.subSequence(endAfterMove, str.length()));
|
.append(str.subSequence(endAfterMove, str.length()));
|
||||||
} else if (moveLength < 0) {
|
} else if (moveLength < 0) {
|
||||||
int startAfterMove = Math.max(startInclude + moveLength, 0);
|
int startAfterMove = Math.max(startInclude + moveLength, 0);
|
||||||
strBuilder.append(str.subSequence(0, startAfterMove))//
|
strBuilder.append(str.subSequence(0, startAfterMove))//
|
||||||
.append(str.subSequence(startInclude, endExclude))//
|
.append(str.subSequence(startInclude, endExclude))//
|
||||||
.append(str.subSequence(startAfterMove, startInclude))//
|
.append(str.subSequence(startAfterMove, startInclude))//
|
||||||
.append(str.subSequence(endExclude, str.length()));
|
.append(str.subSequence(endExclude, str.length()));
|
||||||
} else {
|
} else {
|
||||||
return str(str);
|
return str(str);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,13 +29,13 @@ public class PasswdStrength {
|
|||||||
* 简单密码字典
|
* 简单密码字典
|
||||||
*/
|
*/
|
||||||
private final static String[] DICTIONARY = {"password", "abc123", "iloveyou", "adobe123", "123123", "sunshine",
|
private final static String[] DICTIONARY = {"password", "abc123", "iloveyou", "adobe123", "123123", "sunshine",
|
||||||
"1314520", "a1b2c3", "123qwe", "aaa111", "qweasd", "admin", "passwd"};
|
"1314520", "a1b2c3", "123qwe", "aaa111", "qweasd", "admin", "passwd"};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数字长度
|
* 数字长度
|
||||||
*/
|
*/
|
||||||
private final static int[] SIZE_TABLE = {9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999,
|
private final static int[] SIZE_TABLE = {9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999,
|
||||||
Integer.MAX_VALUE};
|
Integer.MAX_VALUE};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查密码的健壮性
|
* 检查密码的健壮性
|
||||||
@ -65,48 +65,48 @@ public class PasswdStrength {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (len > 4 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0
|
if (len > 4 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0
|
||||||
|| countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0
|
|| countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0
|
||||||
|| countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0
|
|| countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0
|
||||||
|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0
|
|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0
|
||||||
|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0
|
|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0
|
||||||
|| countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {
|
|| countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {
|
||||||
level++;
|
level++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 6 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0
|
if (len > 6 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0
|
||||||
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 || countLetter(passwd, CHAR_TYPE.NUM) > 0
|
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 || countLetter(passwd, CHAR_TYPE.NUM) > 0
|
||||||
&& countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0
|
&& countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0
|
||||||
|| countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0
|
|| countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0
|
||||||
&& countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0 || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0
|
&& countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0 || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0
|
||||||
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {
|
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {
|
||||||
level++;
|
level++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 8 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0
|
if (len > 8 && countLetter(passwd, CHAR_TYPE.NUM) > 0 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) > 0
|
||||||
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {
|
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) > 0 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) > 0) {
|
||||||
level++;
|
level++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 6 && countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3
|
if (len > 6 && countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3
|
||||||
|| countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3
|
|| countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3
|
||||||
|| countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2
|
|| countLetter(passwd, CHAR_TYPE.NUM) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2
|
||||||
|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3
|
|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3
|
||||||
|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2
|
|| countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2
|
||||||
|| countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) {
|
|| countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 3 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) {
|
||||||
level++;
|
level++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 8 && countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2
|
if (len > 8 && countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2
|
||||||
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 || countLetter(passwd, CHAR_TYPE.NUM) >= 2
|
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 || countLetter(passwd, CHAR_TYPE.NUM) >= 2
|
||||||
&& countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2
|
&& countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2
|
||||||
|| countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2
|
|| countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2
|
||||||
&& countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2 || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2
|
&& countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2 || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2
|
||||||
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) {
|
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) {
|
||||||
level++;
|
level++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > 10 && countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2
|
if (len > 10 && countLetter(passwd, CHAR_TYPE.NUM) >= 2 && countLetter(passwd, CHAR_TYPE.SMALL_LETTER) >= 2
|
||||||
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) {
|
&& countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) >= 2 && countLetter(passwd, CHAR_TYPE.OTHER_CHAR) >= 2) {
|
||||||
level++;
|
level++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,19 +124,21 @@ public class PasswdStrength {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decrease points
|
// 判断passwd是否为连续字母(a-z/A-Z)的完整子串
|
||||||
if ("abcdefghijklmnopqrstuvwxyz".indexOf(passwd) > 0 || "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(passwd) > 0) {
|
if ("abcdefghijklmnopqrstuvwxyz".contains(passwd) || "ABCDEFGHIJKLMNOPQRSTUVWXYZ".contains(passwd)) {
|
||||||
level--;
|
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--;
|
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--;
|
level--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (countLetter(passwd, CHAR_TYPE.NUM) == len || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) == len
|
if (countLetter(passwd, CHAR_TYPE.NUM) == len || countLetter(passwd, CHAR_TYPE.SMALL_LETTER) == len
|
||||||
|| countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) == len) {
|
|| countLetter(passwd, CHAR_TYPE.CAPITAL_LETTER) == len) {
|
||||||
level--;
|
level--;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +174,7 @@ public class PasswdStrength {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测密码是否为简单密码字典中的弱密码或包含字典弱密码片段
|
||||||
for (String s : DICTIONARY) {
|
for (String s : DICTIONARY) {
|
||||||
if (passwd.equals(s) || s.contains(passwd)) {
|
if (passwd.equals(s) || s.contains(passwd)) {
|
||||||
level--;
|
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 密码
|
* @param passwd 密码
|
||||||
* @return 密码等级枚举
|
* @return 密码等级枚举
|
||||||
@ -232,8 +235,7 @@ public class PasswdStrength {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check character's type, includes num, capital letter, small letter and other character.
|
* 检查字符类型,包括数字、大写字母、小写字母及其他字符
|
||||||
* 检查字符类型
|
|
||||||
*
|
*
|
||||||
* @param c 字符
|
* @param c 字符
|
||||||
* @return 类型
|
* @return 类型
|
||||||
|
|||||||
@ -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读取异常
|
* @throws IORuntimeException IO读取异常
|
||||||
*/
|
*/
|
||||||
public CsvRow nextRow() throws IORuntimeException {
|
public CsvRow nextRow() throws IORuntimeException {
|
||||||
|
|||||||
@ -71,29 +71,28 @@ public class SplitIter extends ComputeIter<String> implements Serializable {
|
|||||||
return text.substring(offset);
|
return text.substring(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
final int start = finder.start(offset);
|
String result = null;
|
||||||
// 无分隔符,结束
|
int start;
|
||||||
if (start < 0) {
|
do {
|
||||||
// 如果不再有分隔符,但是遗留了字符,则单独作为一个段
|
start = finder.start(offset);
|
||||||
if (offset <= text.length()) {
|
// 无分隔符,结束
|
||||||
final String result = text.substring(offset);
|
if (start < 0) {
|
||||||
if (false == ignoreEmpty || false == result.isEmpty()) {
|
// 如果不再有分隔符,但是遗留了字符,则单独作为一个段
|
||||||
// 返回非空串
|
if (offset <= text.length()) {
|
||||||
offset = Integer.MAX_VALUE;
|
result = text.substring(offset);
|
||||||
return result;
|
if (!ignoreEmpty || !result.isEmpty()) {
|
||||||
|
// 返回非空串
|
||||||
|
offset = Integer.MAX_VALUE;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 找到新的分隔符位置
|
// 找到新的分隔符位置
|
||||||
final String result = text.substring(offset, start);
|
result = text.substring(offset, start);
|
||||||
offset = finder.end(start);
|
offset = finder.end(start);
|
||||||
|
} while (ignoreEmpty && result.isEmpty()); // 空串则继续循环
|
||||||
if (ignoreEmpty && result.isEmpty()) {
|
|
||||||
// 发现空串且需要忽略时,跳过之
|
|
||||||
return computeNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
count++;
|
count++;
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -1082,7 +1082,7 @@ public class ClassUtil {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取class类路径URL, 不管是否在jar包中都会返回文件夹的路径<br>
|
* 获取class类路径URL, 不管是否在jar包中都会返回文件夹的路径<br>
|
||||||
* class在jar包中返回jar所在文件夹,class不在jar中返回文件夹目录<br>
|
* class在jar包中返回jar的路径,class不在jar中返回文件夹目录<br>
|
||||||
* jdk中的类不能使用此方法
|
* jdk中的类不能使用此方法
|
||||||
*
|
*
|
||||||
* @param clazz 类
|
* @param clazz 类
|
||||||
@ -1098,7 +1098,7 @@ public class ClassUtil {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取class类路径, 不管是否在jar包中都会返回文件夹的路径<br>
|
* 获取class类路径, 不管是否在jar包中都会返回文件夹的路径<br>
|
||||||
* class在jar包中返回jar所在文件夹,class不在jar中返回文件夹目录<br>
|
* class在jar包中返回jar的路径,class不在jar中返回文件夹目录<br>
|
||||||
* jdk中的类不能使用此方法
|
* jdk中的类不能使用此方法
|
||||||
*
|
*
|
||||||
* @param clazz 类
|
* @param clazz 类
|
||||||
|
|||||||
@ -100,7 +100,7 @@ public class CreditCodeUtil {
|
|||||||
|
|
||||||
//
|
//
|
||||||
for (int i = 0; i < 2; i++) {
|
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]));
|
buf.append(Character.toUpperCase(BASE_CODE_ARRAY[num]));
|
||||||
}
|
}
|
||||||
for (int i = 2; i < 8; i++) {
|
for (int i = 2; i < 8; i++) {
|
||||||
@ -108,7 +108,7 @@ public class CreditCodeUtil {
|
|||||||
buf.append(BASE_CODE_ARRAY[num]);
|
buf.append(BASE_CODE_ARRAY[num]);
|
||||||
}
|
}
|
||||||
for (int i = 8; i < 17; i++) {
|
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]);
|
buf.append(BASE_CODE_ARRAY[num]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import cn.hutool.core.text.escape.XmlUnescape;
|
|||||||
* 转义和反转义工具类Escape / Unescape<br>
|
* 转义和反转义工具类Escape / Unescape<br>
|
||||||
* escape采用ISO Latin字符集对指定的字符串进行编码。<br>
|
* escape采用ISO Latin字符集对指定的字符串进行编码。<br>
|
||||||
* 所有的空格符、标点符号、特殊字符以及其他非ASCII字符都将被转化成%xx格式的字符编码(xx等于该字符在字符集表里面的编码的16进制数字)。
|
* 所有的空格符、标点符号、特殊字符以及其他非ASCII字符都将被转化成%xx格式的字符编码(xx等于该字符在字符集表里面的编码的16进制数字)。
|
||||||
* TODO 6.x迁移到core.text.escape包下
|
* TODO 7.x迁移到core.text.escape包下
|
||||||
*
|
*
|
||||||
* @author xiaoleilu
|
* @author xiaoleilu
|
||||||
*/
|
*/
|
||||||
@ -20,11 +20,11 @@ public class EscapeUtil {
|
|||||||
* 不转义的符号编码
|
* 不转义的符号编码
|
||||||
*/
|
*/
|
||||||
private static final String NOT_ESCAPE_CHARS = "*@-_+./";
|
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.isDigit(c)
|
||||||
|| Character.isLowerCase(c)
|
|| Character.isLowerCase(c)
|
||||||
|| Character.isUpperCase(c)
|
|| Character.isUpperCase(c)
|
||||||
|| StrUtil.contains(NOT_ESCAPE_CHARS, c)
|
|| StrUtil.contains(NOT_ESCAPE_CHARS, c)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,7 +122,7 @@ public class EscapeUtil {
|
|||||||
char c;
|
char c;
|
||||||
for (int i = 0; i < content.length(); i++) {
|
for (int i = 0; i < content.length(); i++) {
|
||||||
c = content.charAt(i);
|
c = content.charAt(i);
|
||||||
if (false == filter.accept(c)) {
|
if (!filter.accept(c)) {
|
||||||
tmp.append(c);
|
tmp.append(c);
|
||||||
} else if (c < 256) {
|
} else if (c < 256) {
|
||||||
tmp.append("%");
|
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 被转义的内容
|
* @param content 被转义的内容
|
||||||
* @return 解码后的字符串
|
* @return 解码后的字符串
|
||||||
*/
|
*/
|
||||||
public static String unescape(String content) {
|
public static String unescape(final String content) {
|
||||||
if (StrUtil.isBlank(content)) {
|
if (StrUtil.isBlank(content)) {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder tmp = new StringBuilder(content.length());
|
final int len = content.length();
|
||||||
|
final StringBuilder tmp = new StringBuilder(len);
|
||||||
int lastPos = 0;
|
int lastPos = 0;
|
||||||
int pos;
|
int pos;
|
||||||
char ch;
|
char ch;
|
||||||
while (lastPos < content.length()) {
|
while (lastPos < len) {
|
||||||
pos = content.indexOf("%", lastPos);
|
pos = content.indexOf("%", lastPos);
|
||||||
if (pos == lastPos) {
|
if (pos == lastPos) {
|
||||||
if (content.charAt(pos + 1) == 'u') {
|
if (pos + 1 < len && content.charAt(pos + 1) == 'u') {
|
||||||
ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
|
if (pos + 6 <= len) {
|
||||||
tmp.append(ch);
|
ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
|
||||||
lastPos = pos + 6;
|
tmp.append(ch);
|
||||||
|
lastPos = pos + 6;
|
||||||
|
} else {
|
||||||
|
// Not enough characters, append as-is
|
||||||
|
tmp.append(content.substring(pos));
|
||||||
|
lastPos = len;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
|
// Check if there's enough characters for hex escape (%XX)
|
||||||
tmp.append(ch);
|
if (pos + 3 <= len) {
|
||||||
lastPos = pos + 3;
|
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 {
|
} else {
|
||||||
if (pos == -1) {
|
if (pos == -1) {
|
||||||
tmp.append(content.substring(lastPos));
|
tmp.append(content.substring(lastPos));
|
||||||
lastPos = content.length();
|
lastPos = len;
|
||||||
} else {
|
} else {
|
||||||
tmp.append(content, lastPos, pos);
|
tmp.append(content, lastPos, pos);
|
||||||
lastPos = pos;
|
lastPos = pos;
|
||||||
|
|||||||
@ -27,7 +27,7 @@ public class HexUtil {
|
|||||||
* @return 是否为16进制
|
* @return 是否为16进制
|
||||||
*/
|
*/
|
||||||
public static boolean isHexNumber(String value) {
|
public static boolean isHexNumber(String value) {
|
||||||
if(StrUtil.startWith(value, '-')){
|
if (StrUtil.startWith(value, '-')) {
|
||||||
// issue#2875
|
// issue#2875
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ public class HexUtil {
|
|||||||
if (value.startsWith("0x", index) || value.startsWith("0X", index)) {
|
if (value.startsWith("0x", index) || value.startsWith("0X", index)) {
|
||||||
index += 2;
|
index += 2;
|
||||||
} else if (value.startsWith("#", index)) {
|
} else if (value.startsWith("#", index)) {
|
||||||
index ++;
|
index++;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
new BigInteger(value.substring(index), 16);
|
new BigInteger(value.substring(index), 16);
|
||||||
@ -304,7 +304,7 @@ public class HexUtil {
|
|||||||
* @since 5.7.4
|
* @since 5.7.4
|
||||||
*/
|
*/
|
||||||
public static int hexToInt(String value) {
|
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
|
* @since 5.7.4
|
||||||
*/
|
*/
|
||||||
public static long hexToLong(String value) {
|
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) {
|
if (null == hexStr) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new BigInteger(hexStr, 16);
|
return new BigInteger(removeHexPrefix(hexStr), 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -379,17 +379,46 @@ public class HexUtil {
|
|||||||
* @return 格式化后的字符串
|
* @return 格式化后的字符串
|
||||||
*/
|
*/
|
||||||
public static String format(final String hexStr, String prefix) {
|
public static String format(final String hexStr, String prefix) {
|
||||||
|
if (StrUtil.isEmpty(hexStr)) {
|
||||||
|
return StrUtil.EMPTY;
|
||||||
|
}
|
||||||
if (null == prefix) {
|
if (null == prefix) {
|
||||||
prefix = StrUtil.EMPTY;
|
prefix = StrUtil.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int length = hexStr.length();
|
final int length = hexStr.length();
|
||||||
final StringBuilder builder = StrUtil.builder(length + length / 2 + (length / 2 * prefix.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) {
|
for (int i = 0; i < length; i++) {
|
||||||
builder.append(CharUtil.SPACE).append(prefix).append(hexStr.charAt(i)).append(hexStr.charAt(i + 1));
|
if (i % 2 == 0) {
|
||||||
|
if (i != 0) {
|
||||||
|
builder.append(CharUtil.SPACE);
|
||||||
|
}
|
||||||
|
builder.append(prefix);
|
||||||
|
}
|
||||||
|
builder.append(hexStr.charAt(i));
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去除十六进制字符串的常见前缀 0x、0X、#
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,14 +72,14 @@ public class ObjectUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算对象长度,如果是字符串调用其length函数,集合类调用其size函数,数组调用其length属性,其他可遍历对象遍历计算长度<br>
|
* <p>计算对象长度,支持类型包括:
|
||||||
* 支持的类型包括:
|
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>CharSequence</li>
|
* <li>{@code null}:默认返回{@code 0};</li>
|
||||||
* <li>Map</li>
|
* <li>数组:返回数组长度;</li>
|
||||||
* <li>Iterator</li>
|
* <li>{@link CharSequence}:返回{@link CharSequence#length()};</li>
|
||||||
* <li>Enumeration</li>
|
* <li>{@link Collection}:返回{@link Collection#size()};</li>
|
||||||
* <li>Array</li>
|
* <li>{@link Iterator}或{@link Iterable}:可迭代的元素数量;副作用:{@link Iterator}只能被迭代一次</li>
|
||||||
|
* <li>{@link Enumeration}:返回可迭代的元素数量;副作用:{@link Enumeration}只能被迭代一次</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param obj 被计算长度的对象
|
* @param obj 被计算长度的对象
|
||||||
@ -128,7 +128,7 @@ public class ObjectUtil {
|
|||||||
* 对象中是否包含元素<br>
|
* 对象中是否包含元素<br>
|
||||||
* 支持的对象类型包括:
|
* 支持的对象类型包括:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>String</li>
|
* <li>CharSequence</li>
|
||||||
* <li>Collection</li>
|
* <li>Collection</li>
|
||||||
* <li>Map</li>
|
* <li>Map</li>
|
||||||
* <li>Iterator</li>
|
* <li>Iterator</li>
|
||||||
@ -144,11 +144,22 @@ public class ObjectUtil {
|
|||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (obj instanceof String) {
|
if (obj instanceof CharSequence) {
|
||||||
if (element == null) {
|
if (!(element instanceof CharSequence)) {
|
||||||
return false;
|
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) {
|
if (obj instanceof Collection) {
|
||||||
return ((Collection<?>) obj).contains(element);
|
return ((Collection<?>) obj).contains(element);
|
||||||
|
|||||||
@ -888,13 +888,19 @@ public class ReflectUtil {
|
|||||||
return (T) ClassUtil.getPrimitiveDefaultValue(type);
|
return (T) ClassUtil.getPrimitiveDefaultValue(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 某些特殊接口的实例化按照默认实现进行
|
if (Object.class != type) {
|
||||||
if (type.isAssignableFrom(AbstractMap.class)) {
|
// 某些特殊接口的实例化按照默认实现进行
|
||||||
type = (Class<T>) HashMap.class;
|
if (type.isAssignableFrom(AbstractMap.class)) {
|
||||||
} else if (type.isAssignableFrom(List.class)) {
|
type = (Class<T>) HashMap.class;
|
||||||
type = (Class<T>) ArrayList.class;
|
} else if (type.isAssignableFrom(List.class)) {
|
||||||
} else if (type.isAssignableFrom(Set.class)) {
|
type = (Class<T>) ArrayList.class;
|
||||||
type = (Class<T>) HashSet.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 {
|
try {
|
||||||
|
|||||||
@ -187,7 +187,7 @@ public class StrUtil extends CharSequenceUtil implements StrPool {
|
|||||||
/**
|
/**
|
||||||
* 解码字节码
|
* 解码字节码
|
||||||
*
|
*
|
||||||
* @param data 字符串
|
* @param data byte数组
|
||||||
* @param charset 字符集,如果此字段为空,则解码的结果取决于平台
|
* @param charset 字符集,如果此字段为空,则解码的结果取决于平台
|
||||||
* @return 解码后的字符串
|
* @return 解码后的字符串
|
||||||
*/
|
*/
|
||||||
@ -216,7 +216,7 @@ public class StrUtil extends CharSequenceUtil implements StrPool {
|
|||||||
/**
|
/**
|
||||||
* 解码字节码
|
* 解码字节码
|
||||||
*
|
*
|
||||||
* @param data 字符串
|
* @param data Byte数组
|
||||||
* @param charset 字符集,如果此字段为空,则解码的结果取决于平台
|
* @param charset 字符集,如果此字段为空,则解码的结果取决于平台
|
||||||
* @return 解码后的字符串
|
* @return 解码后的字符串
|
||||||
*/
|
*/
|
||||||
@ -261,7 +261,7 @@ public class StrUtil extends CharSequenceUtil implements StrPool {
|
|||||||
if (null == charset) {
|
if (null == charset) {
|
||||||
charset = Charset.defaultCharset();
|
charset = Charset.defaultCharset();
|
||||||
}
|
}
|
||||||
return charset.decode(data).toString();
|
return charset.decode(data.duplicate()).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -34,6 +34,12 @@ public class TypeUtil {
|
|||||||
return (Class<?>) type;
|
return (Class<?>) type;
|
||||||
} else if (type instanceof ParameterizedType) {
|
} else if (type instanceof ParameterizedType) {
|
||||||
return (Class<?>) ((ParameterizedType) type).getRawType();
|
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) {
|
} else if (type instanceof TypeVariable) {
|
||||||
Type[] bounds = ((TypeVariable<?>) type).getBounds();
|
Type[] bounds = ((TypeVariable<?>) type).getBounds();
|
||||||
if (bounds.length == 1) {
|
if (bounds.length == 1) {
|
||||||
|
|||||||
@ -270,7 +270,7 @@ public class URLUtil extends URLEncodeUtil {
|
|||||||
try {
|
try {
|
||||||
return file.toURI().toURL();
|
return file.toURI().toURL();
|
||||||
} catch (MalformedURLException e) {
|
} 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();
|
urls[i] = files[i].toURI().toURL();
|
||||||
}
|
}
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
throw new UtilException(e, "Error occured when get URL!");
|
throw new UtilException(e, "Error occurred when get URL!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls;
|
return urls;
|
||||||
@ -807,8 +807,9 @@ public class URLUtil extends URLEncodeUtil {
|
|||||||
} else {
|
} else {
|
||||||
// 如果资源打在jar包中或来自网络,使用网络请求长度
|
// 如果资源打在jar包中或来自网络,使用网络请求长度
|
||||||
// issue#3226, 来自Spring的AbstractFileResolvingResource
|
// issue#3226, 来自Spring的AbstractFileResolvingResource
|
||||||
|
URLConnection con = null;
|
||||||
try {
|
try {
|
||||||
final URLConnection con = url.openConnection();
|
con = url.openConnection();
|
||||||
useCachesIfNecessary(con);
|
useCachesIfNecessary(con);
|
||||||
if (con instanceof HttpURLConnection) {
|
if (con instanceof HttpURLConnection) {
|
||||||
final HttpURLConnection httpCon = (HttpURLConnection) con;
|
final HttpURLConnection httpCon = (HttpURLConnection) con;
|
||||||
@ -817,6 +818,10 @@ public class URLUtil extends URLEncodeUtil {
|
|||||||
return con.getContentLengthLong();
|
return con.getContentLengthLong();
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
throw new IORuntimeException(e);
|
throw new IORuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
if (con instanceof HttpURLConnection) {
|
||||||
|
((HttpURLConnection) con).disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ public class VersionUtil {
|
|||||||
/**
|
/**
|
||||||
* 当前版本大于待比较版本
|
* 当前版本大于待比较版本
|
||||||
*
|
*
|
||||||
* @param currentVersion 当前本本
|
* @param currentVersion 当前版本
|
||||||
* @param compareVersion 待比较版本
|
* @param compareVersion 待比较版本
|
||||||
* @return true 当前版本大于待比较版本
|
* @return true 当前版本大于待比较版本
|
||||||
*/
|
*/
|
||||||
@ -65,7 +65,7 @@ public class VersionUtil {
|
|||||||
/**
|
/**
|
||||||
* 当前版本大于等于待比较版本
|
* 当前版本大于等于待比较版本
|
||||||
*
|
*
|
||||||
* @param currentVersion 当前本本
|
* @param currentVersion 当前版本
|
||||||
* @param compareVersion 待比较版本
|
* @param compareVersion 待比较版本
|
||||||
* @return true 当前版本大于等于待比较版本
|
* @return true 当前版本大于等于待比较版本
|
||||||
*/
|
*/
|
||||||
@ -76,7 +76,7 @@ public class VersionUtil {
|
|||||||
/**
|
/**
|
||||||
* 当前版本小于待比较版本
|
* 当前版本小于待比较版本
|
||||||
*
|
*
|
||||||
* @param currentVersion 当前本本
|
* @param currentVersion 当前版本
|
||||||
* @param compareVersion 待比较版本
|
* @param compareVersion 待比较版本
|
||||||
* @return true 当前版本小于待比较版本
|
* @return true 当前版本小于待比较版本
|
||||||
*/
|
*/
|
||||||
@ -87,7 +87,7 @@ public class VersionUtil {
|
|||||||
/**
|
/**
|
||||||
* 当前版本小于等于待比较版本
|
* 当前版本小于等于待比较版本
|
||||||
*
|
*
|
||||||
* @param currentVersion 当前本本
|
* @param currentVersion 当前版本
|
||||||
* @param compareVersion 待比较版本
|
* @param compareVersion 待比较版本
|
||||||
* @return true 当前版本小于等于待比较版本
|
* @return true 当前版本小于等于待比较版本
|
||||||
*/
|
*/
|
||||||
@ -105,9 +105,9 @@ public class VersionUtil {
|
|||||||
* matchEl("1.0.2", "1.0.0-1.1.1") == true
|
* matchEl("1.0.2", "1.0.0-1.1.1") == true
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*
|
*
|
||||||
* @param currentVersion 当前本本
|
* @param currentVersion 当前版本
|
||||||
* @param versionEl 版本表达式
|
* @param versionEl 版本表达式
|
||||||
* @return true 当前版本小于等于待比较版本
|
* @return true 当前版本是否满足版本表达式
|
||||||
*/
|
*/
|
||||||
public static boolean matchEl(String currentVersion, String versionEl) {
|
public static boolean matchEl(String currentVersion, String versionEl) {
|
||||||
return matchEl(currentVersion, versionEl, defaultVersionsDelimiter);
|
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
|
* matchEl("1.0.2", "1.0.1,1.0.2-1.1.1", ",") == true
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*
|
*
|
||||||
* @param currentVersion 当前本本
|
* @param currentVersion 当前版本
|
||||||
* @param versionEl 版本表达式(可以匹配多个条件,使用指定的分隔符(默认;)分隔),
|
* @param versionEl 版本表达式(可以匹配多个条件,使用指定的分隔符(默认;)分隔),
|
||||||
* {@code '-'}表示范围包含左右版本,如果 {@code '-'}的左边没有,表示小于等于某个版本号, 右边表示大于等于某个版本号。
|
* {@code '-'}表示范围包含左右版本,如果 {@code '-'}的左边没有,表示小于等于某个版本号, 右边表示大于等于某个版本号。
|
||||||
* 支持比较符号{@code '>'},{@code '<'}, {@code '>='},{@code '<='},{@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>
|
* <li>{@code >=2.0.0, 1.9.8} 表示版本号 大于等于{@code 2.0.0}或 版本{@code 1.9.8}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* @param versionsDelimiter 多表达式分隔符
|
* @param versionsDelimiter 多表达式分隔符
|
||||||
* @return true 当前版本小于等于待比较版本
|
* @return true 当前版本是否满足版本表达式
|
||||||
*/
|
*/
|
||||||
public static boolean matchEl(String currentVersion, String versionEl, String versionsDelimiter) {
|
public static boolean matchEl(String currentVersion, String versionEl, String versionsDelimiter) {
|
||||||
if (StrUtil.isBlank(versionsDelimiter)
|
if (StrUtil.isBlank(versionsDelimiter)
|
||||||
@ -185,9 +185,9 @@ public class VersionUtil {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (StrUtil.contains(el, "-")) {
|
} else if (StrUtil.contains(el, "-")) {
|
||||||
String[] pair = el.split("-");
|
int index = el.indexOf('-');
|
||||||
String left = StrUtil.blankToDefault(StrUtil.trim(pair[0]), "");
|
String left = StrUtil.blankToDefault(StrUtil.trim(el.substring(0, index)), "");
|
||||||
String right = StrUtil.blankToDefault(StrUtil.trim(pair[1]), "");
|
String right = StrUtil.blankToDefault(StrUtil.trim(el.substring(index + 1)), "");
|
||||||
|
|
||||||
boolean leftMatch = StrUtil.isBlank(left) || StrUtil.compareVersion(left, trimmedVersion) <= 0;
|
boolean leftMatch = StrUtil.isBlank(left) || StrUtil.compareVersion(left, trimmedVersion) <= 0;
|
||||||
boolean rightMatch = StrUtil.isBlank(right) || StrUtil.compareVersion(right, trimmedVersion) >= 0;
|
boolean rightMatch = StrUtil.isBlank(right) || StrUtil.compareVersion(right, trimmedVersion) >= 0;
|
||||||
|
|||||||
@ -1516,4 +1516,20 @@ public class CollUtilTest {
|
|||||||
|
|
||||||
assertTrue(CollUtil.containsAll(coll1, coll2));
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,6 +130,33 @@ public class DateUtilTest {
|
|||||||
assertEquals("2020-02-29 12:59:59.000", dateTime.toString(DatePattern.NORM_DATETIME_MS_PATTERN));
|
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
|
@Test
|
||||||
public void ceilingDayTest() {
|
public void ceilingDayTest() {
|
||||||
final String dateStr2 = "2020-02-29 12:59:34";
|
final String dateStr2 = "2020-02-29 12:59:34";
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package cn.hutool.core.io;
|
package cn.hutool.core.io;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -63,4 +64,21 @@ public class BufferUtilTest {
|
|||||||
// 读取剩余部分
|
// 读取剩余部分
|
||||||
assertEquals("cc", StrUtil.utf8Str(BufferUtil.readBytes(buffer)));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,4 +19,12 @@ public class FileNameUtilTest {
|
|||||||
final String s = FileNameUtil.mainName("abc.tar.gz");
|
final String s = FileNameUtil.mainName("abc.tar.gz");
|
||||||
assertEquals("abc", s);
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,9 @@ import cn.hutool.core.thread.ThreadUtil;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import java.util.Map;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
public class SimpleCacheTest {
|
public class SimpleCacheTest {
|
||||||
|
|
||||||
@ -61,4 +62,36 @@ public class SimpleCacheTest {
|
|||||||
assertEquals("aaaValue", cache.get("aaa"));
|
assertEquals("aaaValue", cache.get("aaa"));
|
||||||
IoUtil.close(tester);
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -227,6 +227,19 @@ public class ValidatorTest {
|
|||||||
assertTrue(Validator.isBetween(0.19, 0.1, 0.2));
|
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 转换下,base、min 和 max 是完全相等的,因为 double 精度不够
|
||||||
|
// 预期结果为false,但是因为double 精度不够,导致输出为true
|
||||||
|
assertFalse(Validator.isBetween(base, min, max));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void isCarVinTest() {
|
public void isCarVinTest() {
|
||||||
assertTrue(Validator.isCarVin("LSJA24U62JG269225"));
|
assertTrue(Validator.isCarVin("LSJA24U62JG269225"));
|
||||||
|
|||||||
@ -1,20 +1,23 @@
|
|||||||
package cn.hutool.core.math;
|
package cn.hutool.core.math;
|
||||||
|
|
||||||
import java.util.List;
|
import cn.hutool.core.lang.Console;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
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
|
* @author looly
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class ArrangementTest {
|
public class ArrangementTest {
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 基础测试
|
||||||
|
// ----------------------------------------------------
|
||||||
@Test
|
@Test
|
||||||
public void arrangementTest() {
|
public void arrangementTest() {
|
||||||
long result = Arrangement.count(4, 2);
|
long result = Arrangement.count(4, 2);
|
||||||
@ -30,37 +33,164 @@ public class ArrangementTest {
|
|||||||
assertEquals(64, resultAll);
|
assertEquals(64, resultAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// select 基础测试
|
||||||
|
// ----------------------------------------------------
|
||||||
@Test
|
@Test
|
||||||
public void selectTest() {
|
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);
|
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();
|
List<String[]> selectAll = arrangement.selectAll();
|
||||||
assertEquals(Arrangement.countAll(4), selectAll.size());
|
assertEquals(Arrangement.countAll(4), selectAll.size());
|
||||||
|
|
||||||
|
// m=0,应该返回一个空排列
|
||||||
List<String[]> list2 = arrangement.select(0);
|
List<String[]> list2 = arrangement.select(0);
|
||||||
assertEquals(1, list2.size());
|
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>0,A(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
|
@Test
|
||||||
@Disabled
|
public void iteratorFullTest() {
|
||||||
public void selectTest2() {
|
Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3"});
|
||||||
List<String[]> list = MathUtil.arrangementSelect(new String[] { "1", "1", "3", "4" });
|
|
||||||
for (String[] strings : list) {
|
// 测试全排列的情况
|
||||||
Console.log(strings);
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
package cn.hutool.core.math;
|
package cn.hutool.core.math;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组合单元测试
|
* 组合单元测试
|
||||||
*
|
*
|
||||||
@ -51,4 +53,96 @@ public class CombinationTest {
|
|||||||
List<String[]> list2 = combination.select(0);
|
List<String[]> list2 = combination.select(0);
|
||||||
assertEquals(1, list2.size());
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
package cn.hutool.core.text;
|
package cn.hutool.core.text;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
public class PasswdStrengthTest {
|
public class PasswdStrengthTest {
|
||||||
@Test
|
@Test
|
||||||
public void strengthTest(){
|
public void strengthTest(){
|
||||||
@ -15,4 +16,25 @@ public class PasswdStrengthTest {
|
|||||||
String passwd = "9999999999999";
|
String passwd = "9999999999999";
|
||||||
assertEquals(0, PasswdStrength.check(passwd));
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import cn.hutool.core.text.finder.PatternFinder;
|
|||||||
import cn.hutool.core.text.finder.StrFinder;
|
import cn.hutool.core.text.finder.StrFinder;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@ -153,4 +155,18 @@ public class SplitIterTest {
|
|||||||
assertEquals(1, strings.size());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,4 +66,126 @@ public class EscapeUtilTest {
|
|||||||
final String s = EscapeUtil.unescapeHtml4(str);
|
final String s = EscapeUtil.unescapeHtml4(str);
|
||||||
assertEquals("'some text with single quotes'", s);
|
assertEquals("'some text with single quotes'", s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void escapeXmlTest(){
|
||||||
|
final String a = "<>";
|
||||||
|
final String escape = EscapeUtil.escapeXml(a);
|
||||||
|
assertEquals("<>", 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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package cn.hutool.core.util;
|
|||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,4 +86,65 @@ public class HexUtilTest {
|
|||||||
final String s1 = HexUtil.decodeHexStr(s);
|
final String s1 = HexUtil.decodeHexStr(s);
|
||||||
assertEquals("6", s1);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,13 +4,12 @@ import cn.hutool.core.clone.CloneSupport;
|
|||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.date.DatePattern;
|
import cn.hutool.core.date.DatePattern;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
public class ObjectUtilTest {
|
public class ObjectUtilTest {
|
||||||
|
|
||||||
@ -108,4 +107,74 @@ public class ObjectUtilTest {
|
|||||||
String a = null;
|
String a = null;
|
||||||
assertFalse(ObjectUtil.isNotNull(a));
|
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方法支持String、StringBuilder、StringBuffer
|
||||||
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -151,7 +151,7 @@ public class ReflectUtilTest {
|
|||||||
final TestClass testClass = new TestClass();
|
final TestClass testClass = new TestClass();
|
||||||
final Method method = ReflectUtil.getMethod(TestClass.class, "setA", int.class);
|
final Method method = ReflectUtil.getMethod(TestClass.class, "setA", int.class);
|
||||||
assertThrows(IllegalArgumentException.class,
|
assertThrows(IllegalArgumentException.class,
|
||||||
() -> ReflectUtil.invoke(testClass, method, "NaN"));
|
() -> ReflectUtil.invoke(testClass, method, "NaN"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -210,6 +210,7 @@ public class ReflectUtilTest {
|
|||||||
private String n;
|
private String n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UnusedReturnValue")
|
||||||
public static Method getMethodWithReturnTypeCheck(final Class<?> clazz, final boolean ignoreCase, final String methodName, final Class<?>... paramTypes) throws SecurityException {
|
public static Method getMethodWithReturnTypeCheck(final Class<?> clazz, final boolean ignoreCase, final String methodName, final Class<?>... paramTypes) throws SecurityException {
|
||||||
if (null == clazz || StrUtil.isBlank(methodName)) {
|
if (null == clazz || StrUtil.isBlank(methodName)) {
|
||||||
return null;
|
return null;
|
||||||
@ -220,9 +221,9 @@ public class ReflectUtilTest {
|
|||||||
if (ArrayUtil.isNotEmpty(methods)) {
|
if (ArrayUtil.isNotEmpty(methods)) {
|
||||||
for (final Method method : methods) {
|
for (final Method method : methods) {
|
||||||
if (StrUtil.equals(methodName, method.getName(), ignoreCase)
|
if (StrUtil.equals(methodName, method.getName(), ignoreCase)
|
||||||
&& ClassUtil.isAllAssignableFrom(method.getParameterTypes(), paramTypes)
|
&& ClassUtil.isAllAssignableFrom(method.getParameterTypes(), paramTypes)
|
||||||
&& (res == null
|
&& (res == null
|
||||||
|| res.getReturnType().isAssignableFrom(method.getReturnType()))) {
|
|| res.getReturnType().isAssignableFrom(method.getReturnType()))) {
|
||||||
res = method;
|
res = method;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -300,6 +301,7 @@ public class ReflectUtilTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class C2 extends C1 {
|
class C2 extends C1 {
|
||||||
|
@SuppressWarnings("RedundantMethodOverride")
|
||||||
@Override
|
@Override
|
||||||
public void getA() {
|
public void getA() {
|
||||||
|
|
||||||
@ -307,7 +309,7 @@ public class ReflectUtilTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void newInstanceIfPossibleTest(){
|
public void newInstanceIfPossibleTest() {
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
final int intValue = ReflectUtil.newInstanceIfPossible(int.class);
|
final int intValue = ReflectUtil.newInstanceIfPossible(int.class);
|
||||||
assertEquals(0, intValue);
|
assertEquals(0, intValue);
|
||||||
@ -330,19 +332,19 @@ public class ReflectUtilTest {
|
|||||||
|
|
||||||
public static class JdbcDialects {
|
public static class JdbcDialects {
|
||||||
private static final List<Number> DIALECTS =
|
private static final List<Number> DIALECTS =
|
||||||
Arrays.asList(1L, 2L, 3L);
|
Arrays.asList(1L, 2L, 3L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setFieldValueWithFinalTest() {
|
public void setFieldValueWithFinalTest() {
|
||||||
final String fieldName = "DIALECTS";
|
final String fieldName = "DIALECTS";
|
||||||
final List<Number> dialects =
|
final List<Number> dialects =
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
1,
|
1,
|
||||||
2,
|
2,
|
||||||
3,
|
3,
|
||||||
99
|
99
|
||||||
);
|
);
|
||||||
final Field field = ReflectUtil.getField(JdbcDialects.class, fieldName);
|
final Field field = ReflectUtil.getField(JdbcDialects.class, fieldName);
|
||||||
ReflectUtil.removeFinalModify(field);
|
ReflectUtil.removeFinalModify(field);
|
||||||
ReflectUtil.setFieldValue(JdbcDialects.class, fieldName, dialects);
|
ReflectUtil.setFieldValue(JdbcDialects.class, fieldName, dialects);
|
||||||
@ -351,24 +353,63 @@ public class ReflectUtilTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void issue2625Test(){
|
public void issue2625Test() {
|
||||||
// 内部类继承的情况下父类方法会被定义为桥接方法,因此按照pr#1965@Github判断返回值的继承关系来代替判断桥接。
|
// 内部类继承的情况下父类方法会被定义为桥接方法,因此按照pr#1965@Github判断返回值的继承关系来代替判断桥接。
|
||||||
final Method getThis = ReflectUtil.getMethod(A.C.class, "getThis");
|
final Method getThis = ReflectUtil.getMethod(A.C.class, "getThis");
|
||||||
assertTrue(getThis.isBridge());
|
assertTrue(getThis.isBridge());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("InnerClassMayBeStatic")
|
@SuppressWarnings("InnerClassMayBeStatic")
|
||||||
public class A{
|
public class A {
|
||||||
|
|
||||||
public class C extends B{
|
public class C extends B {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class B{
|
protected class B {
|
||||||
public B getThis(){
|
public B getThis() {
|
||||||
return this;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package cn.hutool.core.util;
|
package cn.hutool.core.util;
|
||||||
|
|
||||||
|
import java.lang.reflect.Array;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -38,7 +40,7 @@ public class TypeUtilTest {
|
|||||||
public void getClasses() {
|
public void getClasses() {
|
||||||
Method method = ReflectUtil.getMethod(Parent.class, "getLevel");
|
Method method = ReflectUtil.getMethod(Parent.class, "getLevel");
|
||||||
Type returnType = TypeUtil.getReturnType(method);
|
Type returnType = TypeUtil.getReturnType(method);
|
||||||
Class clazz = TypeUtil.getClass(returnType);
|
Class<?> clazz = TypeUtil.getClass(returnType);
|
||||||
assertEquals(Level1.class, clazz);
|
assertEquals(Level1.class, clazz);
|
||||||
|
|
||||||
method = ReflectUtil.getMethod(Level1.class, "getId");
|
method = ReflectUtil.getMethod(Level1.class, "getId");
|
||||||
@ -47,6 +49,37 @@ public class TypeUtilTest {
|
|||||||
assertEquals(Object.class, clazz);
|
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 static class TestClass {
|
||||||
public List<String> getList() {
|
public List<String> getList() {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
|
|||||||
@ -76,4 +76,32 @@ class VersionUtilTest {
|
|||||||
@Test
|
@Test
|
||||||
void testMatchEl() {
|
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", "-"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-cron</artifactId>
|
<artifactId>hutool-cron</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-crypto</artifactId>
|
<artifactId>hutool-crypto</artifactId>
|
||||||
@ -18,8 +18,6 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<Automatic-Module-Name>cn.hutool.crypto</Automatic-Module-Name>
|
<Automatic-Module-Name>cn.hutool.crypto</Automatic-Module-Name>
|
||||||
<!-- versions -->
|
|
||||||
<bouncycastle.version>1.78.1</bouncycastle.version>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-db</artifactId>
|
<artifactId>hutool-db</artifactId>
|
||||||
@ -151,7 +151,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.microsoft.sqlserver</groupId>
|
<groupId>com.microsoft.sqlserver</groupId>
|
||||||
<artifactId>mssql-jdbc</artifactId>
|
<artifactId>mssql-jdbc</artifactId>
|
||||||
<version>12.2.0.jre8</version>
|
<version>13.2.1.jre8</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@ -133,7 +133,7 @@ public interface Dialect extends Serializable {
|
|||||||
* @throws SQLException SQL执行异常
|
* @throws SQLException SQL执行异常
|
||||||
*/
|
*/
|
||||||
default PreparedStatement psForCount(Connection conn, Query query) throws SQLException {
|
default PreparedStatement psForCount(Connection conn, Query query) throws SQLException {
|
||||||
return psForCount(conn, SqlBuilder.create().query(query));
|
return psForCount(conn, SqlBuilder.create(getWrapper()).query(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -125,4 +126,27 @@ public class NamedSqlTest {
|
|||||||
assertEquals("select * from user where comment = 'include in text' and id = ?", namedSql.getSql());
|
assertEquals("select * from user where comment = 'include in text' and id = ?", namedSql.getSql());
|
||||||
assertArrayEquals(new int[]{5, 6}, (int[]) namedSql.getParams()[0]);
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
package cn.hutool.db;
|
package cn.hutool.db;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import cn.hutool.core.lang.Console;
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import cn.hutool.db.sql.NamedSql;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
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 单元测试
|
* PostgreSQL 单元测试
|
||||||
@ -46,4 +50,14 @@ public class PostgreTest {
|
|||||||
Entity et=db.get(Entity.create("ctest").set("id", 1));
|
Entity et=db.get(Entity.create("ctest").set("id", 1));
|
||||||
assertEquals("new111",et.getStr("t1"));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,7 +59,7 @@ pass = 123456
|
|||||||
remarks = true
|
remarks = true
|
||||||
|
|
||||||
[postgre]
|
[postgre]
|
||||||
url = jdbc:postgresql://looly.centos:5432/test_hutool
|
url = jdbc:postgresql://localhost:5432/test_hutool
|
||||||
user = postgres
|
user = postgres
|
||||||
pass = 123456
|
pass = 123456
|
||||||
remarks = true
|
remarks = true
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-dfa</artifactId>
|
<artifactId>hutool-dfa</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-extra</artifactId>
|
<artifactId>hutool-extra</artifactId>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import cn.hutool.core.lang.SimpleCache;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.jcraft.jsch.Session;
|
import com.jcraft.jsch.Session;
|
||||||
|
|
||||||
import java.util.Iterator;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,15 +110,17 @@ public enum JschSessionPool {
|
|||||||
*/
|
*/
|
||||||
public void remove(Session session) {
|
public void remove(Session session) {
|
||||||
if (null != session) {
|
if (null != session) {
|
||||||
final Iterator<Entry<String, Session>> iterator = this.cache.iterator();
|
String key = null;
|
||||||
Entry<String, Session> entry;
|
for (Map.Entry<String, Session> entry : cache) {
|
||||||
while (iterator.hasNext()) {
|
|
||||||
entry = iterator.next();
|
|
||||||
if (session.equals(entry.getValue())) {
|
if (session.equals(entry.getValue())) {
|
||||||
iterator.remove();
|
key = entry.getKey();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(null != key){
|
||||||
|
cache.remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-http</artifactId>
|
<artifactId>hutool-http</artifactId>
|
||||||
@ -38,5 +38,11 @@
|
|||||||
<version>1.4.0</version>
|
<version>1.4.0</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.xml.soap</groupId>
|
||||||
|
<artifactId>jakarta.xml.soap-api</artifactId>
|
||||||
|
<version>2.0.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@ -598,16 +598,21 @@ public class HttpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过反射设置方法名,首先设置HttpURLConnection本身的方法名,再检查是否为代理类,如果是,设置带路对象的方法名。
|
* 通过反射设置方法名,首先设置HttpURLConnection本身的方法名,再检查是否为代理类,如果是,设置代理对象的方法名。
|
||||||
* @param method 方法名
|
* @param method 方法名
|
||||||
*/
|
*/
|
||||||
private void reflectSetMethod(Method method){
|
private void reflectSetMethod(Method method){
|
||||||
ReflectUtil.setFieldValue(this.conn, "method", method.name());
|
try {
|
||||||
|
ReflectUtil.setFieldValue(this.conn, "method", method.name());
|
||||||
|
|
||||||
// HttpsURLConnectionImpl实现中,使用了代理类,需要修改被代理类的method方法
|
// HttpsURLConnectionImpl实现中,使用了代理类,需要修改被代理类的method方法
|
||||||
final Object delegate = ReflectUtil.getFieldValue(this.conn, "delegate");
|
final Object delegate = ReflectUtil.getFieldValue(this.conn, "delegate");
|
||||||
if(null != delegate){
|
if(null != delegate){
|
||||||
ReflectUtil.setFieldValue(delegate, "method", method.name());
|
ReflectUtil.setFieldValue(delegate, "method", method.name());
|
||||||
|
}
|
||||||
|
} catch (Exception e){
|
||||||
|
// ignore
|
||||||
|
// https://github.com/chinabugotech/hutool/issues/4109
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --------------------------------------------------------------- Private Method end
|
// --------------------------------------------------------------- Private Method end
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-json</artifactId>
|
<artifactId>hutool-json</artifactId>
|
||||||
|
|||||||
@ -359,6 +359,8 @@ public class JSONUtil {
|
|||||||
}
|
}
|
||||||
if (obj instanceof CharSequence) {
|
if (obj instanceof CharSequence) {
|
||||||
return StrUtil.str((CharSequence) obj);
|
return StrUtil.str((CharSequence) obj);
|
||||||
|
}else if(obj instanceof Boolean || obj instanceof Number) {
|
||||||
|
return obj.toString();
|
||||||
}
|
}
|
||||||
return toJsonStr(parse(obj, jsonConfig));
|
return toJsonStr(parse(obj, jsonConfig));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -290,7 +290,7 @@ public class JSONUtilTest {
|
|||||||
public void issue3540Test() {
|
public void issue3540Test() {
|
||||||
Long userId = 10101010L;
|
Long userId = 10101010L;
|
||||||
final String jsonStr = JSONUtil.toJsonStr(userId);
|
final String jsonStr = JSONUtil.toJsonStr(userId);
|
||||||
assertEquals("{}", jsonStr);
|
assertEquals("10101010", jsonStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-jwt</artifactId>
|
<artifactId>hutool-jwt</artifactId>
|
||||||
@ -20,8 +20,7 @@
|
|||||||
<Automatic-Module-Name>cn.hutool.jwt</Automatic-Module-Name>
|
<Automatic-Module-Name>cn.hutool.jwt</Automatic-Module-Name>
|
||||||
|
|
||||||
<!-- versions -->
|
<!-- versions -->
|
||||||
<bouncycastle.version>1.78.1</bouncycastle.version>
|
<jjwt.version>0.13.0</jjwt.version>
|
||||||
<jjwt.version>0.12.5</jjwt.version>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|||||||
@ -118,12 +118,7 @@ public class JWT implements RegisteredPayload<JWT> {
|
|||||||
* @return this
|
* @return this
|
||||||
*/
|
*/
|
||||||
public JWT setKey(byte[] key) {
|
public JWT setKey(byte[] key) {
|
||||||
// 检查头信息中是否有算法信息
|
return setSigner(StrUtil.nullToDefault(getAlgorithm(), "HS256"), 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -169,6 +164,13 @@ public class JWT implements RegisteredPayload<JWT> {
|
|||||||
*/
|
*/
|
||||||
public JWT setSigner(JWTSigner signer) {
|
public JWT setSigner(JWTSigner signer) {
|
||||||
this.signer = 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;
|
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)) {
|
if (StrUtil.isBlank(algorithm)) {
|
||||||
this.header.setClaim(JWTHeader.ALGORITHM,
|
this.header.setClaim(JWTHeader.ALGORITHM,
|
||||||
AlgorithmUtil.getId(signer.getAlgorithm()));
|
AlgorithmUtil.getId(signer.getAlgorithm()));
|
||||||
@ -410,6 +412,16 @@ public class JWT implements RegisteredPayload<JWT> {
|
|||||||
signer = NoneJWTSigner.NONE;
|
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;
|
final List<String> tokens = this.tokens;
|
||||||
if (CollUtil.isEmpty(tokens)) {
|
if (CollUtil.isEmpty(tokens)) {
|
||||||
throw new JWTException("No token to verify!");
|
throw new JWTException("No token to verify!");
|
||||||
|
|||||||
@ -34,6 +34,42 @@ public class JWTHeader extends Claims {
|
|||||||
*/
|
*/
|
||||||
public JWTHeader() {}
|
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”头信息
|
* 增加“kid”头信息
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package cn.hutool.jwt.signers;
|
package cn.hutool.jwt.signers;
|
||||||
|
|
||||||
import cn.hutool.core.lang.Assert;
|
|
||||||
import cn.hutool.core.util.ReUtil;
|
import cn.hutool.core.util.ReUtil;
|
||||||
|
import cn.hutool.jwt.JWTException;
|
||||||
|
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
@ -232,10 +232,12 @@ public class JWTSignerUtil {
|
|||||||
* @return 签名器
|
* @return 签名器
|
||||||
*/
|
*/
|
||||||
public static JWTSigner createSigner(String algorithmId, byte[] key) {
|
public static JWTSigner createSigner(String algorithmId, byte[] key) {
|
||||||
Assert.notNull(key, "Signer key must be not null!");
|
if (NoneJWTSigner.isNone(algorithmId)) {
|
||||||
|
if(null == key){
|
||||||
if (null == algorithmId || NoneJWTSigner.ID_NONE.equals(algorithmId)) {
|
return none();
|
||||||
return none();
|
}else{
|
||||||
|
throw new JWTException("When key is not null, algorithmId must not be none.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new HMacJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key);
|
return new HMacJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key);
|
||||||
}
|
}
|
||||||
@ -248,10 +250,12 @@ public class JWTSignerUtil {
|
|||||||
* @return 签名器
|
* @return 签名器
|
||||||
*/
|
*/
|
||||||
public static JWTSigner createSigner(String algorithmId, KeyPair keyPair) {
|
public static JWTSigner createSigner(String algorithmId, KeyPair keyPair) {
|
||||||
Assert.notNull(keyPair, "Signer key pair must be not null!");
|
if (NoneJWTSigner.isNone(algorithmId)) {
|
||||||
|
if(null == keyPair){
|
||||||
if (null == algorithmId || NoneJWTSigner.ID_NONE.equals(algorithmId)) {
|
return none();
|
||||||
return none();
|
}else{
|
||||||
|
throw new JWTException("When key is not null, algorithmId must not be none.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// issue3205@Github
|
// issue3205@Github
|
||||||
@ -270,11 +274,14 @@ public class JWTSignerUtil {
|
|||||||
* @return 签名器
|
* @return 签名器
|
||||||
*/
|
*/
|
||||||
public static JWTSigner createSigner(String algorithmId, Key key) {
|
public static JWTSigner createSigner(String algorithmId, Key key) {
|
||||||
Assert.notNull(key, "Signer key must be not null!");
|
if (NoneJWTSigner.isNone(algorithmId)) {
|
||||||
|
if(null == key){
|
||||||
if (null == algorithmId || NoneJWTSigner.ID_NONE.equals(algorithmId)) {
|
return none();
|
||||||
return NoneJWTSigner.NONE;
|
}else{
|
||||||
|
throw new JWTException("When key is not null, algorithmId must not be none.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key instanceof PrivateKey || key instanceof PublicKey) {
|
if (key instanceof PrivateKey || key instanceof PublicKey) {
|
||||||
// issue3205@Github
|
// issue3205@Github
|
||||||
if(ReUtil.isMatch("ES\\d{3}", algorithmId)){
|
if(ReUtil.isMatch("ES\\d{3}", algorithmId)){
|
||||||
|
|||||||
@ -10,10 +10,27 @@ import cn.hutool.core.util.StrUtil;
|
|||||||
*/
|
*/
|
||||||
public class NoneJWTSigner implements JWTSigner {
|
public class NoneJWTSigner implements JWTSigner {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义一个常量ID_NONE,表示没有ID的情况
|
||||||
|
*/
|
||||||
public static final String ID_NONE = "none";
|
public static final String ID_NONE = "none";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建一个NoneJWTSigner实例,用于处理没有签名的JWT
|
||||||
|
*/
|
||||||
public static NoneJWTSigner NONE = new NoneJWTSigner();
|
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
|
@Override
|
||||||
public String sign(String headerBase64, String payloadBase64) {
|
public String sign(String headerBase64, String payloadBase64) {
|
||||||
return StrUtil.EMPTY;
|
return StrUtil.EMPTY;
|
||||||
|
|||||||
@ -19,6 +19,6 @@ public class Issue3732Test {
|
|||||||
|
|
||||||
// 创建 JWT token
|
// 创建 JWT token
|
||||||
String token = JWTUtil.createToken(payload, SIGNER);
|
String token = JWTUtil.createToken(payload, SIGNER);
|
||||||
assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoidGVzdCJ9.pD3Xz41rtXvU3G1c_yS7ir01FXmDvtjjAOU2HYd8MdA", token);
|
assertEquals("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoidGVzdCJ9.eS1hjkb2ympf7Gtnh_Xmzmb29bXt3J-1SyNTLMBipbY", token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
hutool-jwt/src/test/java/cn/hutool/jwt/Issue4105Test.java
Normal file
67
hutool-jwt/src/test/java/cn/hutool/jwt/Issue4105Test.java
Normal 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的JWT,verify()方法总是返回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的JWT,verify()方法总是返回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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ public class IssueI6IS5BTest {
|
|||||||
final JwtToken jwtToken = new JwtToken();
|
final JwtToken jwtToken = new JwtToken();
|
||||||
jwtToken.setIat(iat);
|
jwtToken.setIat(iat);
|
||||||
final String token = JWTUtil.createToken(JSONUtil.parseObj(jwtToken), "123".getBytes(StandardCharsets.UTF_8));
|
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();
|
final JSONObject payloads = JWTUtil.parseToken(token).getPayloads();
|
||||||
assertEquals("{\"iat\":1677772800}", payloads.toString());
|
assertEquals("{\"iat\":1677772800}", payloads.toString());
|
||||||
final JwtToken o = payloads.toBean(JwtToken.class);
|
final JwtToken o = payloads.toBean(JwtToken.class);
|
||||||
@ -38,7 +38,7 @@ public class IssueI6IS5BTest {
|
|||||||
final JwtToken2 jwtToken = new JwtToken2();
|
final JwtToken2 jwtToken = new JwtToken2();
|
||||||
jwtToken.setIat(iat);
|
jwtToken.setIat(iat);
|
||||||
final String token = JWTUtil.createToken(JSONUtil.parseObj(jwtToken), "123".getBytes(StandardCharsets.UTF_8));
|
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();
|
final JSONObject payloads = JWTUtil.parseToken(token).getPayloads();
|
||||||
assertEquals("{\"iat\":1677772800}", payloads.toString());
|
assertEquals("{\"iat\":1677772800}", payloads.toString());
|
||||||
final JwtToken2 o = payloads.toBean(JwtToken2.class);
|
final JwtToken2 o = payloads.toBean(JwtToken2.class);
|
||||||
|
|||||||
@ -19,9 +19,9 @@ public class JWTTest {
|
|||||||
.setExpiresAt(DateUtil.parse("2022-01-01"))
|
.setExpiresAt(DateUtil.parse("2022-01-01"))
|
||||||
.setKey(key);
|
.setKey(key);
|
||||||
|
|
||||||
final String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
|
final String rightToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
|
||||||
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imxvb2x5IiwiYWRtaW4iOnRydWUsImV4cCI6MTY0MDk2NjQwMH0." +
|
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imxvb2x5IiwiYWRtaW4iOnRydWUsImV4cCI6MTY0MDk2NjQwMH0." +
|
||||||
"bXlSnqVeJXWqUIt7HyEhgKNVlIPjkumHlAwFY-5YCtk";
|
"8siIwEMHf-DRyUjVElS_yipb6Mo3c1z0wFiheGXWGQw";
|
||||||
|
|
||||||
final String token = jwt.sign();
|
final String token = jwt.sign();
|
||||||
assertEquals(rightToken, token);
|
assertEquals(rightToken, token);
|
||||||
@ -58,7 +58,7 @@ public class JWTTest {
|
|||||||
.setPayload("admin", true)
|
.setPayload("admin", true)
|
||||||
.setSigner(JWTSignerUtil.none());
|
.setSigner(JWTSignerUtil.none());
|
||||||
|
|
||||||
final String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0." +
|
final String rightToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0." +
|
||||||
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imxvb2x5IiwiYWRtaW4iOnRydWV9.";
|
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Imxvb2x5IiwiYWRtaW4iOnRydWV9.";
|
||||||
|
|
||||||
final String token = jwt.sign();
|
final String token = jwt.sign();
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-log</artifactId>
|
<artifactId>hutool-log</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-poi</artifactId>
|
<artifactId>hutool-poi</artifactId>
|
||||||
|
|||||||
@ -79,7 +79,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
|
|||||||
/**
|
/**
|
||||||
* 自定义需要处理的sheet编号,如果-1表示处理所有sheet
|
* 自定义需要处理的sheet编号,如果-1表示处理所有sheet
|
||||||
*/
|
*/
|
||||||
private int rid = -1;
|
private int sheetIndex = -1;
|
||||||
/**
|
/**
|
||||||
* sheet名称,主要用于使用sheet名读取的情况
|
* sheet名称,主要用于使用sheet名读取的情况
|
||||||
*/
|
*/
|
||||||
@ -132,7 +132,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
|
|||||||
* @throws POIException IO异常包装
|
* @throws POIException IO异常包装
|
||||||
*/
|
*/
|
||||||
public Excel03SaxReader read(POIFSFileSystem fs, String idOrRidOrSheetName) throws POIException {
|
public Excel03SaxReader read(POIFSFileSystem fs, String idOrRidOrSheetName) throws POIException {
|
||||||
this.rid = getSheetIndex(idOrRidOrSheetName);
|
this.sheetIndex = getSheetIndex(idOrRidOrSheetName);
|
||||||
|
|
||||||
formatListener = new FormatTrackingHSSFListener(new MissingRecordAwareHSSFListener(this));
|
formatListener = new FormatTrackingHSSFListener(new MissingRecordAwareHSSFListener(this));
|
||||||
final HSSFRequest request = new HSSFRequest();
|
final HSSFRequest request = new HSSFRequest();
|
||||||
@ -162,7 +162,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
|
|||||||
* @return sheet序号
|
* @return sheet序号
|
||||||
*/
|
*/
|
||||||
public int getSheetIndex() {
|
public int getSheetIndex() {
|
||||||
return this.rid;
|
return this.sheetIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,8 +175,8 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
|
|||||||
return this.sheetName;
|
return this.sheetName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.boundSheetRecords.size() > this.rid) {
|
if (this.boundSheetRecords.size() > this.sheetIndex) {
|
||||||
return this.boundSheetRecords.get(this.rid > -1 ? this.rid : this.curRid).getSheetname();
|
return this.boundSheetRecords.get(this.sheetIndex > -1 ? this.sheetIndex : this.curRid).getSheetname();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -189,7 +189,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void processRecord(Record record) {
|
public void processRecord(Record record) {
|
||||||
if (this.rid > -1 && this.curRid > this.rid) {
|
if (this.sheetIndex > -1 && this.curRid > this.sheetIndex) {
|
||||||
// 指定Sheet之后的数据不再处理
|
// 指定Sheet之后的数据不再处理
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -200,7 +200,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
|
|||||||
boundSheetRecords.add(boundSheetRecord);
|
boundSheetRecords.add(boundSheetRecord);
|
||||||
final String currentSheetName = boundSheetRecord.getSheetname();
|
final String currentSheetName = boundSheetRecord.getSheetname();
|
||||||
if(null != this.sheetName && StrUtil.equals(this.sheetName, currentSheetName)){
|
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) {
|
} else if (record instanceof SSTRecord) {
|
||||||
// 静态字符串表
|
// 静态字符串表
|
||||||
@ -215,7 +215,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
|
|||||||
curRid++;
|
curRid++;
|
||||||
}
|
}
|
||||||
} else if (record instanceof EOFRecord){
|
} 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);
|
throw new POIException("Sheet [{}] not exist!", this.sheetName);
|
||||||
}
|
}
|
||||||
if(this.curRid != -1 && isProcessCurrentSheet()) {
|
if(this.curRid != -1 && isProcessCurrentSheet()) {
|
||||||
@ -369,35 +369,35 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
|
|||||||
*/
|
*/
|
||||||
private boolean isProcessCurrentSheet() {
|
private boolean isProcessCurrentSheet() {
|
||||||
// rid < 0 且 sheet名称存在,说明没有匹配到sheet名称
|
// 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开始
|
* 获取sheet索引,从0开始
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>传入'rId'开头,直接去除rId前缀</li>
|
* <li>传入'rId'开头,直接去除rId前缀</li>
|
||||||
* <li>传入纯数字,表示sheetIndex,直接转换为rid</li>
|
* <li>传入纯数字,表示sheetIndex,直接使用</li>
|
||||||
* </ul>
|
* </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开始
|
* @return sheet索引,从0开始
|
||||||
* @since 5.5.5
|
* @since 5.5.5
|
||||||
*/
|
*/
|
||||||
private int getSheetIndex(String idOrRidOrSheetName) {
|
private int getSheetIndex(String sheetIndexOrSheetName) {
|
||||||
Assert.notBlank(idOrRidOrSheetName, "id or rid or sheetName must be not blank!");
|
Assert.notBlank(sheetIndexOrSheetName, "id or rid or sheetName must be not blank!");
|
||||||
|
|
||||||
// rid直接处理
|
// rid直接处理
|
||||||
if (StrUtil.startWithIgnoreCase(idOrRidOrSheetName, RID_PREFIX)) {
|
if (StrUtil.startWithIgnoreCase(sheetIndexOrSheetName, RID_PREFIX)) {
|
||||||
return Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, RID_PREFIX));
|
return Integer.parseInt(StrUtil.removePrefixIgnoreCase(sheetIndexOrSheetName, RID_PREFIX));
|
||||||
} else if(StrUtil.startWithIgnoreCase(idOrRidOrSheetName, SHEET_NAME_PREFIX)){
|
} else if(StrUtil.startWithIgnoreCase(sheetIndexOrSheetName, SHEET_NAME_PREFIX)){
|
||||||
// since 5.7.10,支持任意名称
|
// since 5.7.10,支持任意名称
|
||||||
this.sheetName = StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, SHEET_NAME_PREFIX);
|
this.sheetName = StrUtil.removePrefixIgnoreCase(sheetIndexOrSheetName, SHEET_NAME_PREFIX);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
return Integer.parseInt(idOrRidOrSheetName);
|
return Integer.parseInt(sheetIndexOrSheetName);
|
||||||
} catch (NumberFormatException ignore) {
|
} catch (NumberFormatException ignore) {
|
||||||
// 如果用于传入非数字,按照sheet名称对待
|
// 如果用于传入非数字,按照sheet名称对待
|
||||||
this.sheetName = idOrRidOrSheetName;
|
this.sheetName = sheetIndexOrSheetName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,12 +13,8 @@ import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
|||||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||||
|
|
||||||
import java.awt.Font;
|
import java.awt.*;
|
||||||
import java.io.Closeable;
|
import java.io.*;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Word docx生成器
|
* Word docx生成器
|
||||||
@ -102,7 +98,19 @@ public class Word07Writer implements Closeable {
|
|||||||
* @return this
|
* @return this
|
||||||
*/
|
*/
|
||||||
public Word07Writer addText(Font font, String... texts) {
|
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
|
* @return this
|
||||||
*/
|
*/
|
||||||
public Word07Writer addText(ParagraphAlignment align, Font font, String... texts) {
|
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();
|
final XWPFParagraph p = this.doc.createParagraph();
|
||||||
if (null != align) {
|
if (null != align) {
|
||||||
p.setAlignment(align);
|
p.setAlignment(align);
|
||||||
@ -129,6 +151,11 @@ public class Word07Writer implements Closeable {
|
|||||||
run.setBold(font.isBold());
|
run.setBold(font.isBold());
|
||||||
run.setItalic(font.isItalic());
|
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;
|
return this;
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,16 +5,20 @@ import cn.hutool.core.collection.ListUtil;
|
|||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.lang.Console;
|
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.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.awt.Font;
|
import java.awt.*;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
public class WordWriterTest {
|
public class WordWriterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -23,6 +27,7 @@ public class WordWriterTest {
|
|||||||
Word07Writer writer = new Word07Writer();
|
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), "我是正文第一部分", "我是正文第二部分");
|
writer.addText(new Font("宋体", Font.PLAIN, 22), "我是正文第一部分", "我是正文第二部分");
|
||||||
|
writer.addText(new Font("宋体", Font.PLAIN, 22), Color.RED, "我是正文第三部分", "我是正文第四部分");
|
||||||
writer.flush(FileUtil.file("e:/wordWrite.docx"));
|
writer.flush(FileUtil.file("e:/wordWrite.docx"));
|
||||||
writer.close();
|
writer.close();
|
||||||
Console.log("OK");
|
Console.log("OK");
|
||||||
@ -95,4 +100,17 @@ public class WordWriterTest {
|
|||||||
word07Writer.addTable(list);
|
word07Writer.addTable(list);
|
||||||
word07Writer.close();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-script</artifactId>
|
<artifactId>hutool-script</artifactId>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-parent</artifactId>
|
<artifactId>hutool-parent</artifactId>
|
||||||
<version>5.8.41</version>
|
<version>5.8.42</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hutool-setting</artifactId>
|
<artifactId>hutool-setting</artifactId>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user