mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-08-18 20:38:02 +08:00
Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
fdf12f38b3 | ||
|
2197da291c | ||
|
4289a2658a | ||
|
010393653d | ||
|
74ea349957 | ||
|
66e2829415 | ||
|
8bc7c997d1 | ||
|
6e382b869e | ||
|
3d49f9bf39 | ||
|
68ef91b6fb | ||
|
2b8284c7fb | ||
|
445703380b | ||
|
e92ab1308c | ||
|
c8f11cd9f9 | ||
|
f3d95e908b | ||
|
8bfefbb9ba | ||
|
3e330f1c64 | ||
|
a27ebb4403 | ||
|
d5bc6216a4 | ||
|
b580030b5b | ||
|
7070c73dbf | ||
|
59f33fd12d | ||
|
4dcbc74cd1 | ||
|
6760658cba | ||
|
0b71a19ba8 | ||
|
cee0efdee1 | ||
|
0661bf27df | ||
|
cae1203591 | ||
|
5244b7510e | ||
|
b5ccc1a7c9 | ||
|
a09da9fd3d | ||
|
3d0d8dea4b | ||
|
325f5a5324 | ||
|
17b22a3b3d | ||
|
40599b028e | ||
|
2b1bf5be59 | ||
|
acac32fd50 | ||
|
56e2852332 | ||
|
7188eab94c | ||
|
b43899c6fb | ||
|
da7d6b9d81 | ||
|
d5bb6b2adb | ||
|
f632ddccb2 | ||
|
0c53623e0c | ||
|
673ab988af | ||
|
d23b9e1f5d | ||
|
d96dc0cd0a | ||
|
10f6278e46 | ||
|
d9369b1402 | ||
|
53214b6fa4 | ||
|
efb04f8a03 | ||
|
66f448d2fc | ||
|
826665618c | ||
|
6469e7bea6 |
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,6 +1,24 @@
|
||||
|
||||
# 🚀Changelog
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
# 5.8.40(2025-08-18)
|
||||
|
||||
### 🐣新特性
|
||||
* 【captcha】 `MathGenerator`四则运算方式支持不生成负数结果(pr#1363@Gitee)
|
||||
* 【core 】 增加`MapValueProvider`和`RecordConverter`并支持Record转换(issue#3985@Github)
|
||||
* 【core 】 `CalendarUtil`增加`isSameYear`和`calendar`方法(issue#3995@Github)
|
||||
* 【core 】 `DateUtil`增加`yyyy-MM-dd'T'HH:mmXXX`格式支持(pr#1367@Gitee)
|
||||
* 【core 】 `MapUtil`增加flatten方法(pr#1368@Gitee)
|
||||
* 【extra 】 `getClientIP`优先获取传入的请求头信息(pr#1373@Gitee)
|
||||
* 【db 】 增加`Gbase8s`驱动支持(issue#ICSFAM@Gitee)
|
||||
|
||||
### 🐞Bug修复
|
||||
* 【extra 】 `Sftp``reconnectIfTimeout`方法改为捕获所有异常(issue#3989@Github)
|
||||
* 【core 】 修复`ChineseDate `闰年闰月节日获取问题(issue#ICL1BT@Gitee)
|
||||
* 【core 】 修复`TreeBuilder`append重复向idTreeMap中put问题(pr#3992@Github)
|
||||
* 【extra 】 修复`QLExpressEngine`allowClassSet无效问题(issue#3994@Github)
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
# 5.8.39(2025-06-20)
|
||||
|
||||
@@ -25,7 +43,7 @@
|
||||
* 【core 】 修复`ClassUti`isNormalClass判断未排除String问题(issue#3965@Github)
|
||||
* 【core 】 修复`ZipUtil`中zlib和unZlib调用后资源未释放问题(issue#3976@Github)
|
||||
* 【core 】 修复`Money`类的setAmount方法没有获取当前币种的小数位数而是使用的默认小数位和在遇到非2小数位的币种(如日元使用 0 位)会导致金额设置错误问题(pr#3970@Github)
|
||||
* 【cahce 】 修复`AbstractCache`putWithoutLock方法可能导致的外部资源泄露问题(pr#3958@Github)
|
||||
* 【cache 】 修复`AbstractCache`putWithoutLock方法可能导致的外部资源泄露问题(pr#3958@Github)
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
# 5.8.38(2025-05-13)
|
||||
|
@@ -134,18 +134,18 @@ Each module can be introduced individually, or all modules can be introduced by
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 🍐Gradle
|
||||
```
|
||||
implementation 'cn.hutool:hutool-all:5.8.39'
|
||||
implementation 'cn.hutool:hutool-all:5.8.40'
|
||||
```
|
||||
|
||||
## 📥Download
|
||||
|
||||
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.39/)
|
||||
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.40/)
|
||||
|
||||
> 🔔️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.
|
||||
|
@@ -124,20 +124,20 @@ Hutool = Hu + tool,是原公司项目底层代码剥离后的开源库,“Hu
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 🍐Gradle
|
||||
```
|
||||
implementation 'cn.hutool:hutool-all:5.8.39'
|
||||
implementation 'cn.hutool:hutool-all:5.8.40'
|
||||
```
|
||||
|
||||
### 📥下载jar
|
||||
|
||||
点击以下链接,下载`hutool-all-X.X.X.jar`即可:
|
||||
|
||||
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.39/)
|
||||
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.8.40/)
|
||||
|
||||
> 🔔️注意
|
||||
> Hutool 5.x支持JDK8+,对Android平台没有测试,不能保证所有工具类或工具方法可用。
|
||||
|
@@ -1 +1 @@
|
||||
5.8.39
|
||||
5.8.40
|
||||
|
@@ -1 +1 @@
|
||||
var version = '5.8.39'
|
||||
var version = '5.8.40'
|
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-ai</artifactId>
|
||||
|
@@ -43,7 +43,11 @@ public enum ModelName {
|
||||
/**
|
||||
* grok
|
||||
*/
|
||||
GROK("grok");
|
||||
GROK("grok"),
|
||||
/**
|
||||
* ollama
|
||||
*/
|
||||
OLLAMA("ollama");
|
||||
|
||||
private final String value;
|
||||
|
||||
|
@@ -192,4 +192,19 @@ public class Models {
|
||||
}
|
||||
}
|
||||
|
||||
// Ollama的模型
|
||||
public enum Ollama {
|
||||
QWEN3_32B("qwen3:32b");
|
||||
|
||||
private final String model;
|
||||
|
||||
Ollama(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.ollama;
|
||||
|
||||
/**
|
||||
* Ollama公共类
|
||||
*
|
||||
* @author yangruoyu-yumeisoft
|
||||
* @since 5.8.40
|
||||
*/
|
||||
public class OllamaCommon {
|
||||
|
||||
/**
|
||||
* Ollama模型格式枚举
|
||||
*/
|
||||
public enum OllamaFormat {
|
||||
/**
|
||||
* JSON格式
|
||||
*/
|
||||
JSON("json"),
|
||||
/**
|
||||
* 无格式
|
||||
*/
|
||||
NONE("");
|
||||
|
||||
private final String format;
|
||||
|
||||
OllamaFormat(String format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public String getFormat() {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ollama选项常量
|
||||
*/
|
||||
public static class Options {
|
||||
/**
|
||||
* 温度参数
|
||||
*/
|
||||
public static final String TEMPERATURE = "temperature";
|
||||
/**
|
||||
* top_p参数
|
||||
*/
|
||||
public static final String TOP_P = "top_p";
|
||||
/**
|
||||
* top_k参数
|
||||
*/
|
||||
public static final String TOP_K = "top_k";
|
||||
/**
|
||||
* 最大token数
|
||||
*/
|
||||
public static final String NUM_PREDICT = "num_predict";
|
||||
/**
|
||||
* 随机种子
|
||||
*/
|
||||
public static final String SEED = "seed";
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.ollama;
|
||||
|
||||
import cn.hutool.ai.Models;
|
||||
import cn.hutool.ai.core.BaseConfig;
|
||||
|
||||
/**
|
||||
* Ollama配置类,初始化API接口地址,设置默认的模型
|
||||
*
|
||||
* @author yangruoyu-yumeisoft
|
||||
* @since 5.8.40
|
||||
*/
|
||||
public class OllamaConfig extends BaseConfig {
|
||||
|
||||
private final String API_URL = "http://localhost:11434";
|
||||
|
||||
private final String DEFAULT_MODEL = Models.Ollama.QWEN3_32B.getModel();
|
||||
|
||||
public OllamaConfig() {
|
||||
setApiUrl(API_URL);
|
||||
setModel(DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
public OllamaConfig(String apiUrl) {
|
||||
this();
|
||||
setApiUrl(apiUrl);
|
||||
}
|
||||
|
||||
public OllamaConfig(String apiUrl, String model) {
|
||||
this();
|
||||
setApiUrl(apiUrl);
|
||||
setModel(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return "ollama";
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.ollama;
|
||||
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.AIServiceProvider;
|
||||
|
||||
/**
|
||||
* 创建Ollama服务实现类
|
||||
*
|
||||
* @author yangruoyu-yumeisoft
|
||||
* @since 5.8.40
|
||||
*/
|
||||
public class OllamaProvider implements AIServiceProvider {
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "ollama";
|
||||
}
|
||||
|
||||
@Override
|
||||
public OllamaService create(final AIConfig config) {
|
||||
return new OllamaServiceImpl(config);
|
||||
}
|
||||
}
|
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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.ollama;
|
||||
|
||||
import cn.hutool.ai.core.AIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Ollama特有的功能
|
||||
*
|
||||
* @author yangruoyu-yumeisoft
|
||||
* @since 5.8.40
|
||||
*/
|
||||
public interface OllamaService extends AIService {
|
||||
|
||||
/**
|
||||
* 生成文本补全
|
||||
*
|
||||
* @param prompt 输入提示
|
||||
* @return AI回答
|
||||
* @since 5.8.40
|
||||
*/
|
||||
String generate(String prompt);
|
||||
|
||||
/**
|
||||
* 生成文本补全-SSE流式输出
|
||||
*
|
||||
* @param prompt 输入提示
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.40
|
||||
*/
|
||||
void generate(String prompt, Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 生成文本补全(带选项)
|
||||
*
|
||||
* @param prompt 输入提示
|
||||
* @param format 响应格式
|
||||
* @return AI回答
|
||||
* @since 5.8.40
|
||||
*/
|
||||
String generate(String prompt, String format);
|
||||
|
||||
/**
|
||||
* 生成文本补全(带选项)-SSE流式输出
|
||||
*
|
||||
* @param prompt 输入提示
|
||||
* @param format 响应格式
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.40
|
||||
*/
|
||||
void generate(String prompt, String format, Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 生成文本嵌入向量
|
||||
*
|
||||
* @param prompt 输入文本
|
||||
* @return 嵌入向量结果
|
||||
* @since 5.8.40
|
||||
*/
|
||||
String embeddings(String prompt);
|
||||
|
||||
/**
|
||||
* 列出本地可用的模型
|
||||
*
|
||||
* @return 模型列表
|
||||
* @since 5.8.40
|
||||
*/
|
||||
String listModels();
|
||||
|
||||
/**
|
||||
* 显示模型信息
|
||||
*
|
||||
* @param modelName 模型名称
|
||||
* @return 模型信息
|
||||
* @since 5.8.40
|
||||
*/
|
||||
String showModel(String modelName);
|
||||
|
||||
/**
|
||||
* 拉取模型
|
||||
*
|
||||
* @param modelName 模型名称
|
||||
* @return 拉取结果
|
||||
* @since 5.8.40
|
||||
*/
|
||||
String pullModel(String modelName);
|
||||
|
||||
/**
|
||||
* 删除模型
|
||||
*
|
||||
* @param modelName 模型名称
|
||||
* @return 删除结果
|
||||
* @since 5.8.40
|
||||
*/
|
||||
String deleteModel(String modelName);
|
||||
|
||||
/**
|
||||
* 复制模型
|
||||
*
|
||||
* @param source 源模型名称
|
||||
* @param destination 目标模型名称
|
||||
* @return 复制结果
|
||||
* @since 5.8.40
|
||||
*/
|
||||
String copyModel(String source, String destination);
|
||||
|
||||
/**
|
||||
* 简化的对话方法
|
||||
*
|
||||
* @param prompt 对话题词
|
||||
* @return AI回答
|
||||
* @since 5.8.40
|
||||
*/
|
||||
default String chat(String prompt) {
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user", prompt));
|
||||
return chat(messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的对话方法-SSE流式输出
|
||||
*
|
||||
* @param prompt 对话题词
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 5.8.40
|
||||
*/
|
||||
default void chat(String prompt, Consumer<String> callback) {
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user", prompt));
|
||||
chat(messages, callback);
|
||||
}
|
||||
}
|
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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.ollama;
|
||||
|
||||
import cn.hutool.ai.AIException;
|
||||
import cn.hutool.ai.core.AIConfig;
|
||||
import cn.hutool.ai.core.BaseAIService;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.bean.BeanPath;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.Header;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Ollama服务,AI具体功能的实现
|
||||
*
|
||||
* @author yangruoyu-yumeisoft
|
||||
* @since 5.8.40
|
||||
*/
|
||||
public class OllamaServiceImpl extends BaseAIService implements OllamaService {
|
||||
|
||||
// 对话补全
|
||||
private static final String CHAT_ENDPOINT = "/api/chat";
|
||||
// 文本生成
|
||||
private static final String GENERATE_ENDPOINT = "/api/generate";
|
||||
// 文本嵌入
|
||||
private static final String EMBEDDINGS_ENDPOINT = "/api/embeddings";
|
||||
// 列出模型
|
||||
private static final String LIST_MODELS_ENDPOINT = "/api/tags";
|
||||
// 显示模型信息
|
||||
private static final String SHOW_MODEL_ENDPOINT = "/api/show";
|
||||
// 拉取模型
|
||||
private static final String PULL_MODEL_ENDPOINT = "/api/pull";
|
||||
// 删除模型
|
||||
private static final String DELETE_MODEL_ENDPOINT = "/api/delete";
|
||||
// 复制模型
|
||||
private static final String COPY_MODEL_ENDPOINT = "/api/copy";
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param config AI配置
|
||||
*/
|
||||
public OllamaServiceImpl(final AIConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
final String paramJson = buildChatRequestBody(messages);
|
||||
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
JSONObject responseJson = JSONUtil.parseObj(response.body());
|
||||
Object errorMessage = BeanPath.create("error").get(responseJson);
|
||||
if(errorMessage!=null){
|
||||
throw new RuntimeException(errorMessage.toString());
|
||||
}
|
||||
return BeanPath.create("message.content").get(responseJson).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(final List<Message> messages, final Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "ollama-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String prompt) {
|
||||
final String paramJson = buildGenerateRequestBody(prompt, null);
|
||||
final HttpResponse response = sendPost(GENERATE_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generate(String prompt, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildGenerateStreamRequestBody(prompt, null);
|
||||
ThreadUtil.newThread(() -> sendPostStream(GENERATE_ENDPOINT, paramMap, callback::accept), "ollama-generate-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String prompt, String format) {
|
||||
final String paramJson = buildGenerateRequestBody(prompt, format);
|
||||
final HttpResponse response = sendPost(GENERATE_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generate(String prompt, String format, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildGenerateStreamRequestBody(prompt, format);
|
||||
ThreadUtil.newThread(() -> sendPostStream(GENERATE_ENDPOINT, paramMap, callback::accept), "ollama-generate-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String embeddings(String prompt) {
|
||||
final String paramJson = buildEmbeddingsRequestBody(prompt);
|
||||
final HttpResponse response = sendPost(EMBEDDINGS_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String listModels() {
|
||||
final HttpResponse response = sendGet(LIST_MODELS_ENDPOINT);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String showModel(String modelName) {
|
||||
final String paramJson = buildShowModelRequestBody(modelName);
|
||||
final HttpResponse response = sendPost(SHOW_MODEL_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pullModel(String modelName) {
|
||||
final String paramJson = buildPullModelRequestBody(modelName);
|
||||
final HttpResponse response = sendPost(PULL_MODEL_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deleteModel(String modelName) {
|
||||
final String paramJson = buildDeleteModelRequestBody(modelName);
|
||||
final HttpResponse response = sendDeleteRequest(DELETE_MODEL_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String copyModel(String source, String destination) {
|
||||
final String paramJson = buildCopyModelRequestBody(source, destination);
|
||||
final HttpResponse response = sendPost(COPY_MODEL_ENDPOINT, paramJson);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream",false);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
// 合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建chatStream请求体
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
// 合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
// 构建generate请求体
|
||||
private String buildGenerateRequestBody(final String prompt, final String format) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
if (StrUtil.isNotBlank(format)) {
|
||||
paramMap.put("format", format);
|
||||
}
|
||||
// 合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建generateStream请求体
|
||||
private Map<String, Object> buildGenerateStreamRequestBody(final String prompt, final String format) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
if (StrUtil.isNotBlank(format)) {
|
||||
paramMap.put("format", format);
|
||||
}
|
||||
// 合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
// 构建embeddings请求体
|
||||
private String buildEmbeddingsRequestBody(final String prompt) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
// 合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建showModel请求体
|
||||
private String buildShowModelRequestBody(final String modelName) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("name", modelName);
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建pullModel请求体
|
||||
private String buildPullModelRequestBody(final String modelName) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("name", modelName);
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建deleteModel请求体
|
||||
private String buildDeleteModelRequestBody(final String modelName) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("name", modelName);
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送DELETE请求
|
||||
*
|
||||
* @param endpoint 请求端点
|
||||
* @param paramJson 请求参数JSON
|
||||
* @return 响应结果
|
||||
*/
|
||||
private HttpResponse sendDeleteRequest(String endpoint, String paramJson) {
|
||||
try {
|
||||
return HttpRequest.delete(config.getApiUrl() + endpoint)
|
||||
.header(Header.CONTENT_TYPE, "application/json")
|
||||
.header(Header.ACCEPT, "application/json")
|
||||
.body(paramJson)
|
||||
.timeout(config.getTimeout())
|
||||
.execute();
|
||||
} catch (Exception e) {
|
||||
throw new AIException("Failed to send DELETE request: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建copyModel请求体
|
||||
private String buildCopyModelRequestBody(final String source, final String destination) {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("source", source);
|
||||
requestBody.put("destination", destination);
|
||||
return JSONUtil.toJsonStr(requestBody);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 对Ollama的封装实现.
|
||||
*
|
||||
* 使用方法:
|
||||
* // 创建AI服务
|
||||
* OllamaService aiService = AIServiceFactory.getAIService(
|
||||
* new AIConfigBuilder(ModelName.OLLAMA.getValue())
|
||||
* .setApiUrl("http://localhost:11434")
|
||||
* .setModel("qwen2.5-coder:32b")
|
||||
* .build(),
|
||||
* OllamaService.class
|
||||
* );
|
||||
*
|
||||
* // 构造上下文
|
||||
* List<Message> messageList=new ArrayList<>();
|
||||
* messageList.add(new Message("system","你是一个疯疯癫癫的机器人"));
|
||||
* messageList.add(new Message("user","你能帮我做什么"));
|
||||
*
|
||||
* // 输出对话结果
|
||||
* System.out.println(aiService.chat(messageList));
|
||||
*
|
||||
* @author yangruoyu-yumeisoft
|
||||
* @since 5.8.40
|
||||
*/
|
||||
|
||||
package cn.hutool.ai.model.ollama;
|
@@ -3,3 +3,4 @@ cn.hutool.ai.model.deepseek.DeepSeekConfig
|
||||
cn.hutool.ai.model.openai.OpenaiConfig
|
||||
cn.hutool.ai.model.doubao.DoubaoConfig
|
||||
cn.hutool.ai.model.grok.GrokConfig
|
||||
cn.hutool.ai.model.ollama.OllamaConfig
|
||||
|
@@ -3,3 +3,4 @@ cn.hutool.ai.model.deepseek.DeepSeekProvider
|
||||
cn.hutool.ai.model.openai.OpenaiProvider
|
||||
cn.hutool.ai.model.doubao.DoubaoProvider
|
||||
cn.hutool.ai.model.grok.GrokProvider
|
||||
cn.hutool.ai.model.ollama.OllamaProvider
|
||||
|
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* 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.ollama;
|
||||
|
||||
import cn.hutool.ai.AIServiceFactory;
|
||||
import cn.hutool.ai.ModelName;
|
||||
import cn.hutool.ai.core.AIConfigBuilder;
|
||||
import cn.hutool.ai.core.Message;
|
||||
import cn.hutool.core.bean.BeanPath;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSON;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* OllamaService
|
||||
*
|
||||
* @author yangruoyu-yumeisoft
|
||||
* @since 5.8.40
|
||||
*/
|
||||
class OllamaServiceTest {
|
||||
// 创建service
|
||||
OllamaService ollamaService = AIServiceFactory.getAIService(
|
||||
new AIConfigBuilder(ModelName.OLLAMA.getValue())
|
||||
// 这里填写Ollama服务的地址
|
||||
.setApiUrl("http://127.0.0.1:11434")
|
||||
// 这里填写使用的模型
|
||||
.setModel("qwen2.5-coder:32b")
|
||||
.build(),
|
||||
OllamaService.class
|
||||
);
|
||||
|
||||
// 假设有一个Java工程师的Agent提示词
|
||||
String javaEngineerPrompt="# 角色 \n" +
|
||||
"你是一位精通Spring Boot 3.0的资深Java全栈工程师,具备以下核心能力: \n" +
|
||||
"- 精通Spring Boot 3.0新特性与最佳实践 \n" +
|
||||
"- 熟练整合Hutool工具包、Redis数据访问、Feign远程调用、FreeMarker模板引擎 \n" +
|
||||
"- 能输出符合工程规范的代码结构和配置文件 \n" +
|
||||
"- 注重代码可读性与注释规范 \n" +
|
||||
"\n" +
|
||||
"# 任务 \n" +
|
||||
"请完成以下编程任务(按优先级排序): \n" +
|
||||
"1. **核心要求** \n" +
|
||||
" - 使用Spring Boot 3.0构建项目 \n" +
|
||||
" - 必须包含以下依赖: \n" +
|
||||
" - `cn.hutool:hutool-all`(最新版) \n" +
|
||||
" - `org.springframework.boot:spring-boot-starter-data-redis` \n" +
|
||||
" - `org.springframework.cloud:spring-cloud-starter-openfeign` \n" +
|
||||
" - `org.springframework.boot:spring-boot-starter-freemarker` \n" +
|
||||
"2. **约束条件** \n" +
|
||||
" - 代码需符合Java 17语法规范 \n" +
|
||||
" - 每个类必须包含Javadoc风格的类注释 \n" +
|
||||
" - 关键方法需添加`@Api`/`@ApiOperation`注解(若涉及接口) \n" +
|
||||
" - Redis操作需使用`RedisTemplate`实现 \n" +
|
||||
"3. **实现流程** \n" +
|
||||
" ``` \n" +
|
||||
" 1. 生成pom.xml依赖配置 \n" +
|
||||
" 2. 创建基础配置类(如RedisConfig) \n" +
|
||||
" 3. 编写Feign客户端接口 \n" +
|
||||
" 4. 实现FreeMarker模板渲染服务 \n" +
|
||||
" 5. 提供完整Controller示例 \n" +
|
||||
" ``` \n" +
|
||||
"\n" +
|
||||
"# 输出要求 \n" +
|
||||
"请以严格Markdown格式输出,每个模块独立代码块: \n" +
|
||||
"```markdown \n" +
|
||||
"## 1. 项目依赖配置(pom.xml片段) \n" +
|
||||
"```xml \n" +
|
||||
"<dependency>...</dependency> \n" +
|
||||
"``` \n" +
|
||||
"\n" +
|
||||
"## 2. Redis配置类 \n" +
|
||||
"```java \n" +
|
||||
"@Configuration \n" +
|
||||
"public class RedisConfig { ... } \n" +
|
||||
"``` \n" +
|
||||
"\n" +
|
||||
"## 3. Feign客户端示例 \n" +
|
||||
"```java \n" +
|
||||
"@FeignClient(name = \"...\") \n" +
|
||||
"public interface ... { ... } \n" +
|
||||
"``` \n" +
|
||||
"\n" +
|
||||
"## 4. FreeMarker模板服务 \n" +
|
||||
"```java \n" +
|
||||
"@Service \n" +
|
||||
"public class TemplateService { ... } \n" +
|
||||
"``` \n" +
|
||||
"\n" +
|
||||
"## 5. 控制器示例 \n" +
|
||||
"```java \n" +
|
||||
"@RestController \n" +
|
||||
"@RequestMapping(\"/example\") \n" +
|
||||
"public class ExampleController { ... } \n" +
|
||||
"``` \n" +
|
||||
"``` \n" +
|
||||
"\n" +
|
||||
"# 示例片段(供格式参考) \n" +
|
||||
"```java \n" +
|
||||
"/** \n" +
|
||||
" * 示例Feign客户端 \n" +
|
||||
" * @since 1.0.0 \n" +
|
||||
" */ \n" +
|
||||
"@FeignClient(name = \"demo-service\", url = \"${demo.service.url}\") \n" +
|
||||
"public interface DemoClient { \n" +
|
||||
"\n" +
|
||||
" @GetMapping(\"/data/{id}\") \n" +
|
||||
" @ApiOperation(\"获取示例数据\") \n" +
|
||||
" ResponseEntity<String> getData(@PathVariable(\"id\") Long id); \n" +
|
||||
"} \n" +
|
||||
"``` \n" +
|
||||
"\n" +
|
||||
"请按此规范输出完整代码结构,确保自动化程序可直接解析生成项目文件。";
|
||||
|
||||
/**
|
||||
* 同步方式调用
|
||||
*/
|
||||
@Test
|
||||
@Disabled
|
||||
void testSimple() {
|
||||
final String answer = ollamaService.chat("写一个疯狂星期四广告词");
|
||||
assertNotNull(answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按流方式输出
|
||||
*/
|
||||
@Test
|
||||
@Disabled
|
||||
void testStream() {
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
AtomicReference<String> errorMessage = new AtomicReference<>();
|
||||
ollamaService.chat("写一个疯狂星期四广告词", data -> {
|
||||
// 输出到控制台
|
||||
JSON streamData = JSONUtil.parse(data);
|
||||
if (streamData.getByPath("error") != null) {
|
||||
isDone.set(true);
|
||||
errorMessage.set(streamData.getByPath("error").toString());
|
||||
return;
|
||||
}
|
||||
|
||||
if ("true".equals(streamData.getByPath("done").toString())) {
|
||||
isDone.set(true);
|
||||
}
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
if (errorMessage.get() != null) {
|
||||
throw new RuntimeException(errorMessage.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带历史上下文的同步方式调用
|
||||
*/
|
||||
@Test
|
||||
@Disabled
|
||||
void testSimpleWithHistory(){
|
||||
List<Message> messageList=new ArrayList<>();
|
||||
messageList.add(new Message("system",javaEngineerPrompt));
|
||||
messageList.add(new Message("user","帮我写一个Java通过Post方式发送JSON给HTTP接口,请求头带有token"));
|
||||
String result = ollamaService.chat(messageList);
|
||||
assertNotNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testStreamWithHistory(){
|
||||
List<Message> messageList=new ArrayList<>();
|
||||
messageList.add(new Message("system",javaEngineerPrompt));
|
||||
messageList.add(new Message("user","帮我写一个Java通过Post方式发送JSON给HTTP接口,请求头带有token"));
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
AtomicReference<String> errorMessage = new AtomicReference<>();
|
||||
ollamaService.chat(messageList, data -> {
|
||||
// 输出到控制台
|
||||
JSON streamData = JSONUtil.parse(data);
|
||||
if (streamData.getByPath("error") != null) {
|
||||
isDone.set(true);
|
||||
errorMessage.set(streamData.getByPath("error").toString());
|
||||
return;
|
||||
}
|
||||
|
||||
if ("true".equals(streamData.getByPath("done").toString())) {
|
||||
isDone.set(true);
|
||||
}
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
if (errorMessage.get() != null) {
|
||||
throw new RuntimeException(errorMessage.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有已经拉取到服务器上的模型
|
||||
*/
|
||||
@Test
|
||||
@Disabled
|
||||
void testListModels(){
|
||||
String models = ollamaService.listModels();
|
||||
JSONArray modelList = JSONUtil.parse(models).getByPath("models", JSONArray.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 让Ollama拉取模型
|
||||
*/
|
||||
@Test
|
||||
@Disabled
|
||||
void testPullModel(){
|
||||
String result = ollamaService.pullModel("qwen2.5:0.5b");
|
||||
List<String> lines = StrUtil.splitTrim(result, "\n");
|
||||
for (String line : lines) {
|
||||
if(line.contains("error")){
|
||||
throw new RuntimeException(JSONUtil.parse(line).getByPath("error").toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 让Ollama删除已经存在的模型
|
||||
*/
|
||||
@Test
|
||||
@Disabled
|
||||
void testDeleteModel(){
|
||||
// 不会返回任何信息
|
||||
ollamaService.deleteModel("qwen2.5:0.5b");
|
||||
}
|
||||
}
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-all</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-aop</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-bloomFilter</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-bom</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-cache</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-captcha</artifactId>
|
||||
|
@@ -16,14 +16,30 @@ public class MathGenerator implements CodeGenerator {
|
||||
|
||||
private static final String operators = "+-*";
|
||||
|
||||
/** 参与计算数字最大长度 */
|
||||
/**
|
||||
* 参与计算数字最大长度
|
||||
*/
|
||||
private final int numberLength;
|
||||
|
||||
/**
|
||||
* 计算结果是否允许负数
|
||||
*/
|
||||
private final boolean resultHasNegativeNumber;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*/
|
||||
public MathGenerator() {
|
||||
this(2);
|
||||
this(2, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param resultHasNegativeNumber 结果是否允许负数
|
||||
*/
|
||||
public MathGenerator(boolean resultHasNegativeNumber) {
|
||||
this(2, resultHasNegativeNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,22 +48,43 @@ public class MathGenerator implements CodeGenerator {
|
||||
* @param numberLength 参与计算最大数字位数
|
||||
*/
|
||||
public MathGenerator(int numberLength) {
|
||||
this(numberLength, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param numberLength 参与计算最大数字位数
|
||||
* @param resultHasNegativeNumber 结果是否允许负数
|
||||
*/
|
||||
public MathGenerator(int numberLength, boolean resultHasNegativeNumber) {
|
||||
this.numberLength = numberLength;
|
||||
this.resultHasNegativeNumber = resultHasNegativeNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate() {
|
||||
final int limit = getLimit();
|
||||
String number1 = Integer.toString(RandomUtil.randomInt(limit));
|
||||
String number2 = Integer.toString(RandomUtil.randomInt(limit));
|
||||
char operator = RandomUtil.randomChar(operators);
|
||||
int numberInt1 = 0;
|
||||
int numberInt2 = 0;
|
||||
numberInt1 = RandomUtil.randomInt(limit);
|
||||
// 如果禁止了结果有负数,且计算方式正好计算为减法,需要第二个数小于第一个数
|
||||
if (!resultHasNegativeNumber && CharUtil.equals('-', operator, false)) {
|
||||
//如果第一个数为0,第二个数必须为0,随机[0,0)的数字会报错
|
||||
numberInt2 = numberInt1 == 0 ? 0 : RandomUtil.randomInt(0, numberInt1);
|
||||
} else {
|
||||
numberInt2 = RandomUtil.randomInt(limit);
|
||||
}
|
||||
String number1 = Integer.toString(numberInt1);
|
||||
String number2 = Integer.toString(numberInt2);
|
||||
number1 = StrUtil.padAfter(number1, this.numberLength, CharUtil.SPACE);
|
||||
number2 = StrUtil.padAfter(number2, this.numberLength, CharUtil.SPACE);
|
||||
|
||||
return StrUtil.builder()//
|
||||
.append(number1)//
|
||||
.append(RandomUtil.randomChar(operators))//
|
||||
.append(number2)//
|
||||
.append('=').toString();
|
||||
.append(number1)//
|
||||
.append(operator)//
|
||||
.append(number2)//
|
||||
.append('=').toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package cn.hutool.captcha;
|
||||
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.core.math.Calculator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class GeneratorTest {
|
||||
@@ -11,5 +12,13 @@ public class GeneratorTest {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
mathGenerator.verify(mathGenerator.generate(), "0");
|
||||
}
|
||||
|
||||
final MathGenerator mathGenerator1 = new MathGenerator(false);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
String generate = mathGenerator1.generate();
|
||||
if( Calculator.conversion(generate) < 0){
|
||||
throw new RuntimeException("No Pass");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-core</artifactId>
|
||||
|
@@ -0,0 +1,38 @@
|
||||
package cn.hutool.core.bean.copier.provider;
|
||||
|
||||
import cn.hutool.core.bean.copier.ValueProvider;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Map值提供者
|
||||
*
|
||||
* @author Looly
|
||||
* @since 5.8.40
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class MapValueProvider implements ValueProvider<String> {
|
||||
|
||||
private final Map map;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param map map
|
||||
*/
|
||||
public MapValueProvider(final Map map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object value(String key, Type valueType) {
|
||||
return Convert.convert(valueType, map.get(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(String key) {
|
||||
return map.containsKey(key);
|
||||
}
|
||||
}
|
@@ -469,7 +469,7 @@ public class CollUtil {
|
||||
*
|
||||
* @param collection 集合
|
||||
* @param value 需要查找的值
|
||||
* @return 果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true}
|
||||
* @return 如果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true}
|
||||
* @since 5.7.16
|
||||
*/
|
||||
public static boolean safeContains(Collection<?> collection, Object value) {
|
||||
|
@@ -12,8 +12,8 @@ import java.util.Map;
|
||||
* 抽象转换器,提供通用的转换逻辑,同时通过convertInternal实现对应类型的专属逻辑<br>
|
||||
* 转换器不会抛出转换异常,转换失败时会返回{@code null}
|
||||
*
|
||||
* @param <T> 转换的目标类型
|
||||
* @author Looly
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractConverter<T> implements Converter<T>, Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
@@ -22,7 +22,7 @@ public abstract class AbstractConverter<T> implements Converter<T>, Serializable
|
||||
* 不抛异常转换<br>
|
||||
* 当转换失败时返回默认值
|
||||
*
|
||||
* @param value 被转换的值
|
||||
* @param value 被转换的值
|
||||
* @param defaultValue 默认值
|
||||
* @return 转换后的值
|
||||
* @since 4.5.7
|
||||
@@ -59,7 +59,7 @@ public abstract class AbstractConverter<T> implements Converter<T>, Serializable
|
||||
return ((null == result) ? defaultValue : result);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
StrUtil.format("Default value [{}]({}) is not the instance of [{}]", defaultValue, defaultValue.getClass(), targetType));
|
||||
StrUtil.format("Default value [{}]({}) is not the instance of [{}]", defaultValue, defaultValue.getClass(), targetType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ public abstract class AbstractConverter<T> implements Converter<T>, Serializable
|
||||
return value.toString();
|
||||
} else if (ArrayUtil.isArray(value)) {
|
||||
return ArrayUtil.toString(value);
|
||||
} else if(CharUtil.isChar(value)) {
|
||||
} else if (CharUtil.isChar(value)) {
|
||||
//对于ASCII字符使用缓存加速转换,减少空间创建
|
||||
return CharUtil.toString((char)value);
|
||||
return CharUtil.toString((char) value);
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package cn.hutool.core.convert;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.bean.RecordUtil;
|
||||
import cn.hutool.core.convert.impl.*;
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.lang.Opt;
|
||||
@@ -346,6 +347,12 @@ public class ConverterRegistry implements Serializable {
|
||||
return ReflectUtil.newInstanceIfPossible(rowType);
|
||||
}
|
||||
|
||||
// record
|
||||
// issue#3985@Github since 5.8.40
|
||||
if(RecordUtil.isRecord(rowType)){
|
||||
return (T) new RecordConverter(rowType).convert(value, defaultValue);
|
||||
}
|
||||
|
||||
// 表示非需要特殊转换的对象
|
||||
return null;
|
||||
}
|
||||
|
@@ -0,0 +1,48 @@
|
||||
package cn.hutool.core.convert.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.bean.RecordUtil;
|
||||
import cn.hutool.core.bean.copier.ValueProvider;
|
||||
import cn.hutool.core.bean.copier.provider.BeanValueProvider;
|
||||
import cn.hutool.core.bean.copier.provider.MapValueProvider;
|
||||
import cn.hutool.core.convert.ConvertException;
|
||||
import cn.hutool.core.convert.Converter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Record转换器
|
||||
*
|
||||
* @author looly
|
||||
*/
|
||||
public class RecordConverter implements Converter<Object> {
|
||||
|
||||
private final Class<?> recordClass;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
* @param recordClass Record类
|
||||
*/
|
||||
public RecordConverter(Class<?> recordClass) {
|
||||
this.recordClass = recordClass;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object convert(Object value, Object defaultValue) throws IllegalArgumentException {
|
||||
ValueProvider<String> valueProvider = null;
|
||||
if (value instanceof ValueProvider) {
|
||||
valueProvider = (ValueProvider<String>) value;
|
||||
} else if (value instanceof Map) {
|
||||
valueProvider = new MapValueProvider((Map<String, ?>) value);
|
||||
} else if (BeanUtil.isReadableBean(value.getClass())) {
|
||||
valueProvider = new BeanValueProvider(value, false, false);
|
||||
}
|
||||
|
||||
if (null != valueProvider) {
|
||||
return RecordUtil.newInstance(recordClass, valueProvider);
|
||||
}
|
||||
|
||||
throw new ConvertException("Unsupported source type: [{}] to [{}]", value.getClass(), recordClass);
|
||||
}
|
||||
}
|
@@ -74,6 +74,21 @@ public class CalendarUtil {
|
||||
return cal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为指定时区的Calendar,返回新的Calendar
|
||||
*
|
||||
* @param calendar 时间
|
||||
* @param timeZone 新时区
|
||||
* @return 指定时区的新的calendar对象
|
||||
* @since 5.8.30
|
||||
*/
|
||||
public static Calendar calendar(Calendar calendar, final TimeZone timeZone) {
|
||||
// 转换到统一时区,例如UTC
|
||||
calendar = (Calendar) calendar.clone();
|
||||
calendar.setTimeZone(timeZone);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为上午
|
||||
*
|
||||
@@ -437,6 +452,30 @@ public class CalendarUtil {
|
||||
cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA);
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个日期是否为同一年<br>
|
||||
* 同一个年的意思是:ERA(公元)、year(年)都一致。
|
||||
*
|
||||
* @param cal1 日期1
|
||||
* @param cal2 日期2
|
||||
* @return 是否为同一年
|
||||
* @since 5.8.30
|
||||
*/
|
||||
public static boolean isSameYear(final Calendar cal1, Calendar cal2) {
|
||||
if (cal1 == null || cal2 == null) {
|
||||
throw new IllegalArgumentException("The date must not be null");
|
||||
}
|
||||
|
||||
if (!ObjUtil.equals(cal1.getTimeZone(), cal2.getTimeZone())) {
|
||||
// 统一时区
|
||||
cal2 = calendar(cal2, cal1.getTimeZone());
|
||||
}
|
||||
|
||||
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && //
|
||||
// issue#3011@Github
|
||||
cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>检查两个Calendar时间戳是否相同。</p>
|
||||
*
|
||||
|
@@ -329,7 +329,7 @@ public class ChineseDate {
|
||||
* @return 获得农历节日
|
||||
*/
|
||||
public String getFestivals() {
|
||||
return StrUtil.join(",", LunarFestival.getFestivals(this.year, this.month, day));
|
||||
return StrUtil.join(",", LunarFestival.getFestivals(this.year, this.isLeapMonth ? this.month - 1 : this.month, day));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -285,6 +285,15 @@ public class DatePattern {
|
||||
*/
|
||||
public static final FastDateFormat UTC_WITH_XXX_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_WITH_XXX_OFFSET_PATTERN);
|
||||
|
||||
/**
|
||||
* UTC时间:yyyy-MM-dd'T'HH:mmXXX
|
||||
*/
|
||||
public static final String UTC_SIMPLE_MINUTE_WITH_XXX_OFFSET_PATTERN = "yyyy-MM-dd'T'HH:mmXXX";
|
||||
/**
|
||||
* UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mmXXX
|
||||
*/
|
||||
public static final FastDateFormat UTC_SIMPLE_MINUTE_WITH_XXX_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_SIMPLE_MINUTE_WITH_XXX_OFFSET_PATTERN);
|
||||
|
||||
/**
|
||||
* UTC时间:yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
|
||||
*/
|
||||
|
@@ -901,6 +901,9 @@ public class DateUtil extends CalendarUtil {
|
||||
// 带毫秒,格式类似:2018-09-13T05:34:31.999+08:00
|
||||
iso8601String = normalizeMillSeconds(iso8601String, ".", "+");
|
||||
return parse(iso8601String, DatePattern.UTC_MS_WITH_XXX_OFFSET_FORMAT);
|
||||
} else if (iso8601String.length() == 22 && StrUtil.count(iso8601String, ':') == 2) {
|
||||
// 精确到分钟,格式类似:2025-07-28T20:00+08:00
|
||||
return parse(iso8601String, DatePattern.UTC_SIMPLE_MINUTE_WITH_XXX_OFFSET_FORMAT);
|
||||
} else {
|
||||
// 格式类似:2018-09-13T05:34:31+08:00
|
||||
return parse(iso8601String, DatePattern.UTC_WITH_XXX_OFFSET_FORMAT);
|
||||
|
@@ -188,7 +188,9 @@ public class TreeBuilder<E> implements Builder<Tree<E>> {
|
||||
}
|
||||
|
||||
}
|
||||
return append(map);
|
||||
// this.idTreeMap重复put
|
||||
// return append(map);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,7 +226,9 @@ public class TreeBuilder<E> implements Builder<Tree<E>> {
|
||||
}
|
||||
|
||||
}
|
||||
return append(map);
|
||||
// this.idTreeMap重复put
|
||||
// return append(map);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1544,4 +1544,47 @@ public class MapUtil {
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多层级Map处理为一个层级Map类型
|
||||
*
|
||||
* @param map 入参Map
|
||||
* @param <K> 键类型
|
||||
* @param <V> 值类型
|
||||
* @return 单层级Map返回值
|
||||
* @since 5.8.40
|
||||
*/
|
||||
public static <K, V> Map<K, V> flatten(final Map<K, V> map) {
|
||||
return flatten(map, new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归调用将多层级Map处理为一个层级Map类型
|
||||
*
|
||||
* @param map 入参Map
|
||||
* @param flatMap 单层级Map返回值
|
||||
* @param <K> 键类型
|
||||
* @param <V> 值类型
|
||||
* @return 单层级Map返回值
|
||||
* @since 5.8.40
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <K, V> Map<K, V> flatten(final Map<K, V> map, Map<K, V> flatMap) {
|
||||
Assert.notNull(map);
|
||||
if (null == flatMap) {
|
||||
flatMap = new HashMap<>();
|
||||
}
|
||||
|
||||
Map<K, V> finalFlatMap = flatMap;
|
||||
map.forEach((k, v) -> {
|
||||
// 避免嵌套循环
|
||||
if (v instanceof Map && v != map) {
|
||||
flatten((Map<K, V>) v, finalFlatMap);
|
||||
} else {
|
||||
finalFlatMap.put(k, v);
|
||||
}
|
||||
});
|
||||
|
||||
return flatMap;
|
||||
}
|
||||
}
|
||||
|
@@ -1823,7 +1823,7 @@ public class NumberUtil {
|
||||
* @param bigNum1 数字1
|
||||
* @param bigNum2 数字2
|
||||
* @return 是否大于等于
|
||||
* @since 3, 0.9
|
||||
* @since 3.0.9
|
||||
*/
|
||||
public static boolean isGreaterOrEqual(BigDecimal bigNum1, BigDecimal bigNum2) {
|
||||
Assert.notNull(bigNum1);
|
||||
@@ -1837,7 +1837,7 @@ public class NumberUtil {
|
||||
* @param bigNum1 数字1
|
||||
* @param bigNum2 数字2
|
||||
* @return 是否小于
|
||||
* @since 3, 0.9
|
||||
* @since 3.0.9
|
||||
*/
|
||||
public static boolean isLess(BigDecimal bigNum1, BigDecimal bigNum2) {
|
||||
Assert.notNull(bigNum1);
|
||||
@@ -1851,7 +1851,7 @@ public class NumberUtil {
|
||||
* @param bigNum1 数字1
|
||||
* @param bigNum2 数字2
|
||||
* @return 是否小于等于
|
||||
* @since 3, 0.9
|
||||
* @since 3.0.9
|
||||
*/
|
||||
public static boolean isLessOrEqual(BigDecimal bigNum1, BigDecimal bigNum2) {
|
||||
Assert.notNull(bigNum1);
|
||||
|
@@ -0,0 +1,42 @@
|
||||
package cn.hutool.core.bean;
|
||||
|
||||
import cn.hutool.core.annotation.Alias;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class IssueICHM3OTest {
|
||||
@Test
|
||||
public void testMapToBean() {
|
||||
Map<Object,Object> map = MapUtil.builder()
|
||||
.put("doctor_name", "李医生")
|
||||
.put("doctor_id_card_value", "12345")
|
||||
.put("gender", "男")
|
||||
.build();
|
||||
TestClass doctor = BeanUtil.toBean(map, TestClass.class);
|
||||
assertTrue(StrUtil.equals(doctor.name, "李医生"), "姓名不一致");
|
||||
assertTrue(StrUtil.equals(doctor.idCardValue, "12345"), "证件号不一致");
|
||||
|
||||
|
||||
Map<String,Object> mapData = BeanUtil.beanToMap(doctor, true, false);
|
||||
assertTrue(StrUtil.equals(mapData.get("doctor_name").toString(), "李医生"), "姓名不一致");
|
||||
assertTrue(StrUtil.equals(mapData.get("doctor_id_card_value").toString(), "12345"), "证件号不一致");
|
||||
}
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public static class TestClass {
|
||||
@Alias("doctor_name")
|
||||
private String name;
|
||||
@Alias("doctor_id_card_value")
|
||||
private String idCardValue;
|
||||
@Alias("doctor_name")
|
||||
private String gender;
|
||||
}
|
||||
}
|
@@ -4,13 +4,14 @@ import cn.hutool.core.map.MapUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* CollectionStream测试方法
|
||||
*/
|
||||
@@ -19,17 +20,17 @@ public class CollStreamUtilTest {
|
||||
@Test
|
||||
public void testToIdentityMap() {
|
||||
Map<Long, Student> map = CollStreamUtil.toIdentityMap(null, Student::getStudentId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
List<Student> list = new ArrayList<>();
|
||||
map = CollStreamUtil.toIdentityMap(list, Student::getStudentId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
list.add(new Student(1, 1, 1, "张三"));
|
||||
list.add(new Student(1, 1, 2, "李四"));
|
||||
list.add(new Student(1, 1, 3, "王五"));
|
||||
map = CollStreamUtil.toIdentityMap(list, Student::getStudentId);
|
||||
assertEquals(map.get(1L).getName(), "张三");
|
||||
assertEquals(map.get(2L).getName(), "李四");
|
||||
assertEquals(map.get(3L).getName(), "王五");
|
||||
assertEquals("张三", map.get(1L).getName());
|
||||
assertEquals("李四", map.get(2L).getName());
|
||||
assertEquals("王五", map.get(3L).getName());
|
||||
assertNull(map.get(4L));
|
||||
|
||||
// 测试value为空时
|
||||
@@ -41,17 +42,17 @@ public class CollStreamUtilTest {
|
||||
@Test
|
||||
public void testToMap() {
|
||||
Map<Long, String> map = CollStreamUtil.toMap(null, Student::getStudentId, Student::getName);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
List<Student> list = new ArrayList<>();
|
||||
map = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
list.add(new Student(1, 1, 1, "张三"));
|
||||
list.add(new Student(1, 1, 2, "李四"));
|
||||
list.add(new Student(1, 1, 3, "王五"));
|
||||
map = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);
|
||||
assertEquals(map.get(1L), "张三");
|
||||
assertEquals(map.get(2L), "李四");
|
||||
assertEquals(map.get(3L), "王五");
|
||||
assertEquals("张三", map.get(1L));
|
||||
assertEquals("李四", map.get(2L));
|
||||
assertEquals("王五", map.get(3L));
|
||||
assertNull(map.get(4L));
|
||||
|
||||
// 测试value为空时
|
||||
@@ -60,13 +61,47 @@ public class CollStreamUtilTest {
|
||||
assertNull(map.get(4L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToMap_KeyCollision_SilentlyOverwrite() {
|
||||
List<Student> list = new ArrayList<>();
|
||||
list.add(new Student(1, 101, 1, "张三"));
|
||||
list.add(new Student(1, 102, 1, "李四"));
|
||||
Map<Long, String> map = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName, false);
|
||||
|
||||
assertEquals(1, map.size());
|
||||
assertEquals("李四", map.get(1L)); // 确保后面的值覆盖前面的
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToMap_NullKeyOrValue() {
|
||||
List<Student> list = new ArrayList<>();
|
||||
list.add(new Student(1, 1, 1L, "张三"));
|
||||
list.add(null);
|
||||
list.add(new Student(1, 2, 2L, null));
|
||||
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToMap_LargeInputPerformance() {
|
||||
List<Student> list = new ArrayList<>();
|
||||
for (long i = 0; i < 10000; i++) {
|
||||
list.add(new Student(1, 1, i, "学生" + i));
|
||||
}
|
||||
Map<Long, String> map = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);
|
||||
|
||||
assertEquals(10000, map.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGroupByKey() {
|
||||
Map<Long, List<Student>> map = CollStreamUtil.groupByKey(null, Student::getClassId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
List<Student> list = new ArrayList<>();
|
||||
map = CollStreamUtil.groupByKey(list, Student::getClassId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
list.add(new Student(1, 1, 1, "张三"));
|
||||
list.add(new Student(1, 2, 2, "李四"));
|
||||
list.add(new Student(2, 1, 1, "擎天柱"));
|
||||
@@ -92,10 +127,10 @@ public class CollStreamUtilTest {
|
||||
@Test
|
||||
public void testGroupBy2Key() {
|
||||
Map<Long, Map<Long, List<Student>>> map = CollStreamUtil.groupBy2Key(null, Student::getTermId, Student::getClassId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
List<Student> list = new ArrayList<>();
|
||||
map = CollStreamUtil.groupBy2Key(list, Student::getTermId, Student::getClassId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
list.add(new Student(1, 1, 1, "张三"));
|
||||
list.add(new Student(1, 2, 2, "李四"));
|
||||
list.add(new Student(1, 2, 3, "王五"));
|
||||
@@ -131,11 +166,11 @@ public class CollStreamUtilTest {
|
||||
@Test
|
||||
public void testGroup2Map() {
|
||||
Map<Long, Map<Long, Student>> map = CollStreamUtil.group2Map(null, Student::getTermId, Student::getClassId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
|
||||
List<Student> list = new ArrayList<>();
|
||||
map = CollStreamUtil.group2Map(list, Student::getTermId, Student::getClassId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
list.add(new Student(1, 1, 1, "张三"));
|
||||
list.add(new Student(1, 2, 1, "李四"));
|
||||
list.add(new Student(2, 2, 1, "王五"));
|
||||
@@ -152,7 +187,10 @@ public class CollStreamUtilTest {
|
||||
|
||||
// 对null友好
|
||||
Map<Long, Map<Long, Student>> termIdClassIdStudentMap = CollStreamUtil.group2Map(Arrays.asList(null, new Student(2, 2, 1, "王五")), Student::getTermId, Student::getClassId);
|
||||
Map<Long, Map<Long, Student>> termIdClassIdStudentCompareMap = new HashMap<Long, Map<Long, Student>>() {{
|
||||
Map<Long, Map<Long, Student>> termIdClassIdStudentCompareMap = new HashMap<Long, Map<Long, Student>>() {
|
||||
private static final long serialVersionUID = -26683057599474572L;
|
||||
|
||||
{
|
||||
put(null, MapUtil.of(null, null));
|
||||
put(2L, MapUtil.of(2L, new Student(2, 2, 1, "王五")));
|
||||
}};
|
||||
@@ -162,11 +200,11 @@ public class CollStreamUtilTest {
|
||||
@Test
|
||||
public void testGroupKeyValue() {
|
||||
Map<Long, List<Long>> map = CollStreamUtil.groupKeyValue(null, Student::getTermId, Student::getClassId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
|
||||
List<Student> list = new ArrayList<>();
|
||||
map = CollStreamUtil.groupKeyValue(list, Student::getTermId, Student::getClassId);
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
list.add(new Student(1, 1, 1, "张三"));
|
||||
list.add(new Student(1, 2, 1, "李四"));
|
||||
list.add(new Student(2, 2, 1, "王五"));
|
||||
@@ -184,12 +222,12 @@ public class CollStreamUtilTest {
|
||||
|
||||
// 参数null测试
|
||||
Map<Long, List<Student>> map = CollStreamUtil.groupBy(null, Student::getTermId, Collectors.toList());
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
|
||||
// 参数空数组测试
|
||||
List<Student> list = new ArrayList<>();
|
||||
map = CollStreamUtil.groupBy(list, Student::getTermId, Collectors.toList());
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
|
||||
// 放入元素
|
||||
list.add(new Student(1, 1, 1, "张三"));
|
||||
@@ -230,10 +268,10 @@ public class CollStreamUtilTest {
|
||||
@Test
|
||||
public void testTranslate2List() {
|
||||
List<String> list = CollStreamUtil.toList(null, Student::getName);
|
||||
assertEquals(list, Collections.EMPTY_LIST);
|
||||
assertEquals(Collections.EMPTY_LIST, list);
|
||||
List<Student> students = new ArrayList<>();
|
||||
list = CollStreamUtil.toList(students, Student::getName);
|
||||
assertEquals(list, Collections.EMPTY_LIST);
|
||||
assertEquals(Collections.EMPTY_LIST, list);
|
||||
students.add(new Student(1, 1, 1, "张三"));
|
||||
students.add(new Student(1, 2, 2, "李四"));
|
||||
students.add(new Student(2, 1, 1, "李四"));
|
||||
@@ -252,10 +290,10 @@ public class CollStreamUtilTest {
|
||||
@Test
|
||||
public void testTranslate2Set() {
|
||||
Set<String> set = CollStreamUtil.toSet(null, Student::getName);
|
||||
assertEquals(set, Collections.EMPTY_SET);
|
||||
assertEquals(Collections.EMPTY_SET, set);
|
||||
List<Student> students = new ArrayList<>();
|
||||
set = CollStreamUtil.toSet(students, Student::getName);
|
||||
assertEquals(set, Collections.EMPTY_SET);
|
||||
assertEquals(Collections.EMPTY_SET, set);
|
||||
students.add(new Student(1, 1, 1, "张三"));
|
||||
students.add(new Student(1, 2, 2, "李四"));
|
||||
students.add(new Student(2, 1, 1, "李四"));
|
||||
@@ -274,7 +312,7 @@ public class CollStreamUtilTest {
|
||||
Map<Long, Student> map1 = null;
|
||||
Map<Long, Student> map2 = Collections.emptyMap();
|
||||
Map<Long, String> map = CollStreamUtil.merge(map1, map2, (s1, s2) -> s1.getName() + s2.getName());
|
||||
assertEquals(map, Collections.EMPTY_MAP);
|
||||
assertEquals(Collections.EMPTY_MAP, map);
|
||||
map1 = new HashMap<>();
|
||||
map1.put(1L, new Student(1, 1, 1, "张三"));
|
||||
map = CollStreamUtil.merge(map1, map2, this::merge);
|
||||
|
@@ -62,6 +62,25 @@ public class CollUtilTest {
|
||||
assertEquals(srcList, answerList);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPadLeft_NegativeMinLen_ShouldNotModifyList() {
|
||||
List<String> list = CollUtil.newArrayList("a", "b", "c");
|
||||
List<String> original = CollUtil.newArrayList("a", "b", "c");
|
||||
|
||||
CollUtil.padLeft(list, -5, "x");
|
||||
|
||||
assertEquals(original, list, "List should remain unchanged when minLen is negative");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPadLeft_EmptyList_MinLenZero() {
|
||||
List<String> list = CollUtil.newArrayList();
|
||||
|
||||
CollUtil.padLeft(list, 0, "x");
|
||||
|
||||
assertTrue(list.isEmpty(), "List should remain empty when minLen is 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPadRight() {
|
||||
final List<String> srcList = CollUtil.newArrayList("a");
|
||||
@@ -212,6 +231,19 @@ public class CollUtilTest {
|
||||
assertEquals("[1]", r2.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubtractWithDuplicates() {
|
||||
Collection<String> coll1 = new ArrayList<>(Arrays.asList("a", "b", "b", "c"));
|
||||
Collection<String> coll2 = Collections.singletonList("b");
|
||||
Collection<String> result = CollUtil.subtract(coll1, coll2);
|
||||
|
||||
List<String> expected = Arrays.asList("a", "c");
|
||||
List<String> resultList = new ArrayList<>(result);
|
||||
Collections.sort(resultList);
|
||||
Collections.sort(expected);
|
||||
assertEquals(expected, resultList);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toMapListAndToListMapTest() {
|
||||
final HashMap<String, String> map1 = new HashMap<>();
|
||||
@@ -825,6 +857,34 @@ public class CollUtilTest {
|
||||
assertEquals(2, i);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lastIndexOf_NoMatchExists() {
|
||||
List<String> list = CollUtil.newArrayList("a", "b", "c");
|
||||
int idx = CollUtil.lastIndexOf(list, item -> item.equals("z"));
|
||||
assertEquals(-1, idx);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lastIndexOf_MatcherIsNull_MatchAll() {
|
||||
List<String> list = CollUtil.newArrayList("x", "y", "z");
|
||||
int idx = CollUtil.lastIndexOf(list, null);
|
||||
assertEquals(2, idx);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lastIndexOf_EmptyCollection() {
|
||||
List<String> list = CollUtil.newArrayList();
|
||||
int idx = CollUtil.lastIndexOf(list, Objects::nonNull);
|
||||
assertEquals(-1, idx);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lastIndexOf_SingletonCollection_Match() {
|
||||
List<String> list = CollUtil.newArrayList("foo");
|
||||
int idx = CollUtil.lastIndexOf(list, item -> item.equals("foo"));
|
||||
assertEquals(0, idx);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pageTest() {
|
||||
final List<Dict> objects = CollUtil.newArrayList();
|
||||
@@ -907,7 +967,7 @@ public class CollUtilTest {
|
||||
// 测试集合1和集合2不包含相同元素的情况
|
||||
final List<String> list1 = Arrays.asList("a", "b", "c");
|
||||
final List<String> list2 = Arrays.asList("d", "e", "f");
|
||||
|
||||
|
||||
// 期望结果:返回集合1的完整拷贝
|
||||
final List<String> result = CollUtil.subtractToList(list1, list2);
|
||||
assertEquals(3, result.size());
|
||||
@@ -916,11 +976,11 @@ public class CollUtilTest {
|
||||
assertEquals("c", result.get(2));
|
||||
assertEquals(list1, result);
|
||||
assertNotSame(list1, result); // 确保返回的是拷贝而不是原始引用
|
||||
|
||||
|
||||
// 测试集合1中有重复元素的情况
|
||||
final List<String> list3 = Arrays.asList("a", "a", "b", "b", "c");
|
||||
final List<String> list4 = Arrays.asList("d", "e", "f");
|
||||
|
||||
|
||||
// 期望结果:返回集合1的完整拷贝,包括重复元素
|
||||
final List<String> result2 = CollUtil.subtractToList(list3, list4);
|
||||
assertEquals(5, result2.size());
|
||||
@@ -931,11 +991,11 @@ public class CollUtilTest {
|
||||
assertEquals("c", result2.get(4));
|
||||
assertEquals(list3, result2);
|
||||
assertNotSame(list3, result2);
|
||||
|
||||
|
||||
// 测试不同类型的元素但确保两个集合的泛型类型一致
|
||||
final List<Integer> list5 = Arrays.asList(1, 2, 3);
|
||||
final List<Integer> list6 = Arrays.asList(4, 5, 6);
|
||||
|
||||
|
||||
// 期望结果:返回集合1的完整拷贝
|
||||
final List<Integer> result3 = CollUtil.subtractToList(list5, list6);
|
||||
assertEquals(3, result3.size());
|
||||
@@ -1227,9 +1287,9 @@ public class CollUtilTest {
|
||||
genderMap.put(5, "小孩");
|
||||
genderMap.put(6, "男");
|
||||
|
||||
assertEquals(people.get(1).getGender(), "woman");
|
||||
assertEquals("woman", people.get(1).getGender());
|
||||
CollUtil.setValueByMap(people, genderMap, Person::getId, Person::setGender);
|
||||
assertEquals(people.get(1).getGender(), "妇女");
|
||||
assertEquals("妇女", people.get(1).getGender());
|
||||
|
||||
final Map<Integer, Person> personMap = new HashMap<>();
|
||||
personMap.put(1, new Person("AA", 21, "男", 1));
|
||||
@@ -1245,7 +1305,7 @@ public class CollUtilTest {
|
||||
x.setAge(y.getAge());
|
||||
});
|
||||
|
||||
assertEquals(people.get(1).getGender(), "小孩");
|
||||
assertEquals("小孩", people.get(1).getGender());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package cn.hutool.core.convert;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -356,4 +357,10 @@ public class NumberChineseFormatterTest {
|
||||
final String format = NumberChineseFormatter.format(new BigDecimal("3.1415926"), false, false);
|
||||
assertEquals("三点一四一五九二六", format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void issue3986Test() {
|
||||
final String format = NumberChineseFormatter.format(100000.0, true, true);
|
||||
Assertions.assertEquals("壹拾万元整", format);
|
||||
}
|
||||
}
|
||||
|
@@ -667,6 +667,12 @@ public class DateUtilTest {
|
||||
dateStr = dt.toString(simpleDateFormat);
|
||||
assertEquals("2018-09-13 13:34:39.999", dateStr);
|
||||
|
||||
dateStr1 = "2025-07-28T20:00+08:00";
|
||||
dt = DateUtil.parse(dateStr1);
|
||||
assert dt != null;
|
||||
dateStr = dt.toString();
|
||||
assertEquals("2025-07-28 20:00:00", dateStr);
|
||||
|
||||
// 使用UTC时区
|
||||
dateStr1 = "2018-09-13T13:34:39.99";
|
||||
dt = DateUtil.parse(dateStr1);
|
||||
|
@@ -0,0 +1,21 @@
|
||||
package cn.hutool.core.date.chinese;
|
||||
|
||||
import cn.hutool.core.date.ChineseDate;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class IssueICL1BTTest {
|
||||
@Test
|
||||
void getFestivalsTest(){
|
||||
String date = "2025-07-31";
|
||||
Date productionDate = DateUtil.parseDate( date);
|
||||
ChineseDate chineseDate = new ChineseDate(productionDate);
|
||||
System.out.println(chineseDate.isLeapMonth());
|
||||
Assertions.assertTrue(chineseDate.isLeapMonth());
|
||||
String festivals = chineseDate.getFestivals();
|
||||
Assertions.assertEquals("", festivals);
|
||||
}
|
||||
}
|
@@ -528,6 +528,44 @@ public class FileUtilTest {
|
||||
assertTrue(FileUtil.isSub(file, file2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSub_SubIsAncestorOfParentTest() {
|
||||
File parent = new File("d:/home/user/docs/notes");
|
||||
File sub = new File("d:/home/user/docs");
|
||||
assertFalse(FileUtil.isSub(parent, sub));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSub_SamePathTest() {
|
||||
File parent = new File("d:/home/user/docs");
|
||||
File sub = new File("d:/home/user/docs");
|
||||
assertTrue(FileUtil.isSub(parent, sub));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSub_NonexistentPathsTest() {
|
||||
File parent = new File("d:/unlikely/to/exist/parent");
|
||||
File sub = new File("d:/unlikely/to/exist/parent/child/file.txt");
|
||||
assertTrue(FileUtil.isSub(parent, sub));
|
||||
|
||||
File nonchild = new File("d:/also/unlikely/path.txt");
|
||||
assertFalse(FileUtil.isSub(parent, nonchild));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSub_NullParentTest() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
FileUtil.isSub(null, new java.io.File("d:/any/path"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSub_NullSubTest() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
FileUtil.isSub(new java.io.File("d:/any/path"), null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void appendLinesTest(){
|
||||
|
@@ -862,4 +862,29 @@ public class MapUtilTest {
|
||||
assertEquals(0, MapUtil.get(map, "age", new TypeReference<Integer>() {
|
||||
}, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenMapReturnsTest() {
|
||||
Map<String, String> clothes = new HashMap<>();
|
||||
clothes.put("clothesName", "ANTA");
|
||||
clothes.put("clothesPrice", "200");
|
||||
|
||||
Map<String, Object> person = new HashMap<>();
|
||||
person.put("personName", "XXXX");
|
||||
person.put("clothes", clothes);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("home", "AAA");
|
||||
map.put("person", person);
|
||||
|
||||
Map<String, Object> flattenMap = MapUtil.flatten(map);
|
||||
assertEquals("ANTA", MapUtil.get(flattenMap, "clothesName", new TypeReference<String>() {
|
||||
}));
|
||||
assertEquals("200", MapUtil.get(flattenMap, "clothesPrice", new TypeReference<String>() {
|
||||
}));
|
||||
assertEquals("XXXX", MapUtil.get(flattenMap, "personName", new TypeReference<String>() {
|
||||
}));
|
||||
assertEquals("AAA", MapUtil.get(flattenMap, "home", new TypeReference<String>() {
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package cn.hutool.core.text.csv;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class IssueICRMKATest {
|
||||
@Test
|
||||
public void issueICRMAKTest() {
|
||||
CsvReader reader = CsvUtil.getReader();
|
||||
CsvData data = reader.read(ResourceUtil.getUtf8Reader("issueICRMKA.csv"));
|
||||
final CsvRow row = data.getRow(1);
|
||||
Assertions.assertEquals("6.3\" Google Pixel 9 Pro 128 GB Beige", row.get(0));
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package cn.hutool.core.util;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class IssueICOJVZTest {
|
||||
@Test
|
||||
void toUnderlineTest(){
|
||||
String field = "PAGE_NAME";
|
||||
field = StrUtil.toUnderlineCase(field).toUpperCase();
|
||||
Assertions.assertEquals("PAGE_NAME", field);
|
||||
}
|
||||
}
|
2
hutool-core/src/test/resources/issueICRMKA.csv
Normal file
2
hutool-core/src/test/resources/issueICRMKA.csv
Normal file
@@ -0,0 +1,2 @@
|
||||
config,price,unit,package_name,package,equipment,pic_path,crawler_date,set_Discount,installment_details,installment_price,installment_time
|
||||
6.3" Google Pixel 9 Pro 128 GB Beige,94999,₽,fwefwe,fewfew,"Cores - 8x(3.1 GHz), 16 GB, 1 SIM, OLED, 2856x1280, 50+48+48 MP camera, NFC, 5G, GPS, 4700 mAh more details",["https://xx.png"],2025/8/1 4:05,,gerwgrweg,fwefw,fwefwe
|
Can't render this file because it contains an unexpected character in line 2 and column 4.
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-cron</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-crypto</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-db</artifactId>
|
||||
|
@@ -173,6 +173,9 @@ public class DialectFactory implements DriverNamePool {
|
||||
} else if (nameContainsProductInfo.contains("sap")) {
|
||||
// sap hana
|
||||
driver = DRIVER_HANA;
|
||||
} else if (nameContainsProductInfo.contains("gbasedbt-sqli")) {
|
||||
// Gbase8s,见:https://www.gbase.cn/community/post/4029
|
||||
driver = DRIVER_GBASE8S;
|
||||
}
|
||||
|
||||
return driver;
|
||||
|
@@ -104,6 +104,11 @@ public interface DriverNamePool {
|
||||
* JDBC 驱动 南大通用
|
||||
*/
|
||||
String DRIVER_GBASE = "com.gbase.jdbc.Driver";
|
||||
/**
|
||||
* JDBC 驱动 南大通用8S<br>
|
||||
* 见:https://www.gbase.cn/community/post/4029
|
||||
*/
|
||||
String DRIVER_GBASE8S = "com.gbasedbt.jdbc.Driver";
|
||||
/**
|
||||
* JDBC 驱动 神州数据库
|
||||
*/
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-dfa</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-extra</artifactId>
|
||||
|
@@ -4,7 +4,10 @@ import cn.hutool.extra.expression.ExpressionEngine;
|
||||
import cn.hutool.extra.expression.ExpressionException;
|
||||
import com.ql.util.express.DefaultContext;
|
||||
import com.ql.util.express.ExpressRunner;
|
||||
import com.ql.util.express.config.QLExpressRunStrategy;
|
||||
|
||||
import javax.naming.InitialContext;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -24,10 +27,24 @@ public class QLExpressEngine implements ExpressionEngine {
|
||||
*/
|
||||
public QLExpressEngine() {
|
||||
engine = new ExpressRunner();
|
||||
|
||||
// issue#3994@Github
|
||||
// Enforce blacklisting of high-risk method invocations
|
||||
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
|
||||
// Explicitly forbid JNDI lookup calls through InitialContext
|
||||
QLExpressRunStrategy.addSecurityRiskMethod(InitialContext.class, "doLookup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object eval(final String expression, final Map<String, Object> context, Collection<Class<?>> allowClassSet) {
|
||||
// issue#3994@Github
|
||||
if (null != allowClassSet) {
|
||||
for (Class<?> clazz : allowClassSet) {
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
QLExpressRunStrategy.addSecureMethod(clazz, method.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
final DefaultContext<String, Object> defaultContext = new DefaultContext<>();
|
||||
defaultContext.putAll(context);
|
||||
try {
|
||||
|
@@ -27,7 +27,7 @@ import com.github.stuxuhai.jpinyin.PinyinHelper;
|
||||
*/
|
||||
public class JPinyinEngine implements PinyinEngine {
|
||||
|
||||
//设置汉子拼音输出的格式
|
||||
//设置汉字拼音输出的格式
|
||||
PinyinFormat format;
|
||||
|
||||
public JPinyinEngine(){
|
||||
|
@@ -33,7 +33,7 @@ import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombi
|
||||
*/
|
||||
public class Pinyin4jEngine implements PinyinEngine {
|
||||
|
||||
//设置汉子拼音输出的格式
|
||||
//设置汉字拼音输出的格式
|
||||
HanyuPinyinOutputFormat format;
|
||||
|
||||
/**
|
||||
|
@@ -215,7 +215,7 @@ public class JakartaServletUtil {
|
||||
public static String getClientIP(HttpServletRequest request, String... otherHeaderNames) {
|
||||
String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
|
||||
if (ArrayUtil.isNotEmpty(otherHeaderNames)) {
|
||||
headers = ArrayUtil.addAll(headers, otherHeaderNames);
|
||||
headers = ArrayUtil.addAll(otherHeaderNames, headers);
|
||||
}
|
||||
|
||||
return getClientIPByHeader(request, headers);
|
||||
|
@@ -215,7 +215,7 @@ public class ServletUtil {
|
||||
public static String getClientIP(HttpServletRequest request, String... otherHeaderNames) {
|
||||
String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
|
||||
if (ArrayUtil.isNotEmpty(otherHeaderNames)) {
|
||||
headers = ArrayUtil.addAll(headers, otherHeaderNames);
|
||||
headers = ArrayUtil.addAll(otherHeaderNames, headers);
|
||||
}
|
||||
|
||||
return getClientIPByHeader(request, headers);
|
||||
|
@@ -226,7 +226,7 @@ public class Sftp extends AbstractFtp {
|
||||
}
|
||||
try {
|
||||
this.cd(StrUtil.SLASH);
|
||||
} catch (FtpException e) {
|
||||
} catch (Exception e) {
|
||||
close();
|
||||
init();
|
||||
}
|
||||
@@ -692,7 +692,9 @@ public class Sftp extends AbstractFtp {
|
||||
@Override
|
||||
public void close() {
|
||||
JschUtil.close(this.channel);
|
||||
this.channel = null;
|
||||
JschUtil.close(this.session);
|
||||
this.session = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-http</artifactId>
|
||||
|
@@ -391,7 +391,7 @@ public class HttpServerRequest extends HttpServerBase {
|
||||
public String getClientIP(String... otherHeaderNames) {
|
||||
String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
|
||||
if (ArrayUtil.isNotEmpty(otherHeaderNames)) {
|
||||
headers = ArrayUtil.addAll(headers, otherHeaderNames);
|
||||
headers = ArrayUtil.addAll(otherHeaderNames, headers);
|
||||
}
|
||||
|
||||
return getClientIPByHeader(headers);
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-json</artifactId>
|
||||
|
@@ -3,18 +3,18 @@ package cn.hutool.json;
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.XmlUtil;
|
||||
import cn.hutool.json.xml.JSONXMLSerializer;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static javax.xml.xpath.XPathConstants.STRING;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class IssueI676IT {
|
||||
|
||||
@Test
|
||||
public void parseXMLTest() {
|
||||
final JSONObject jsonObject = JSONUtil.parseObj(ResourceUtil.readUtf8Str("issueI676IT.json"));
|
||||
final JSONObject jsonObject = JSONUtil.parseObj(ResourceUtil.readUtf8Str("IssueI676IT.json"));
|
||||
String xmlStr = JSONXMLSerializer.toXml(jsonObject, null, (String) null);
|
||||
String content = String.valueOf(XmlUtil.getByXPath("/page/orderItems[1]/content", XmlUtil.readXML(xmlStr), STRING));
|
||||
assertEquals(content, "bar1");
|
||||
assertEquals("bar1", content);
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-jwt</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-log</artifactId>
|
||||
|
@@ -7,6 +7,11 @@ import cn.hutool.log.Log;
|
||||
import cn.hutool.log.LogFactory;
|
||||
import cn.hutool.log.level.Level;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 日志门面单元测试
|
||||
* @author Looly
|
||||
@@ -45,4 +50,45 @@ public class LogTest {
|
||||
log.info(null);
|
||||
log.warn(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parameterizedMessageEdgeCasesTest() {
|
||||
Log log = LogFactory.get();
|
||||
|
||||
// 测试不同数量的参数
|
||||
log.info("No parameters");
|
||||
log.info("One: {}", "param1");
|
||||
log.info("Two: {} and {}", "param1", "param2");
|
||||
log.info("Three: {}, {}, {}", "param1", "param2", "param3");
|
||||
log.info("Four: {}, {}, {}, {}", "param1", "param2", "param3", "param4");
|
||||
|
||||
// 测试参数不足的情况
|
||||
log.info("Missing param: {} and {}", "only_one");
|
||||
|
||||
// 测试参数过多的情况
|
||||
log.info("Extra param: {}", "param1", "extra_param");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void i18nMessageTest() {
|
||||
Log log = LogFactory.get();
|
||||
// 国际化消息测试
|
||||
log.info("中文消息测试");
|
||||
log.info("Message with unicode: {}", "特殊字符©®™✓✗★☆");
|
||||
log.info("多语言混排: 中文, English, 日本語, 한글");
|
||||
log.info("Emoji测试: 😀🚀🌏");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void complexObjectTest() {
|
||||
Log log = LogFactory.get();
|
||||
// 复杂对象参数测试
|
||||
List<String> list = Arrays.asList("item1", "item2");
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("key", "value");
|
||||
|
||||
log.info("List: {}", list);
|
||||
log.info("Map: {}", map);
|
||||
log.info("Null object: {}", (Object)null);
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-poi</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-script</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-setting</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-socket</artifactId>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-system</artifactId>
|
||||
|
2
pom.xml
2
pom.xml
@@ -8,7 +8,7 @@
|
||||
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.8.39</version>
|
||||
<version>5.8.40-SNAPSHOT</version>
|
||||
<name>hutool</name>
|
||||
<description>Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。</description>
|
||||
<url>https://github.com/chinabugotech/hutool</url>
|
||||
|
Reference in New Issue
Block a user