Compare commits

...

26 Commits

Author SHA1 Message Date
xy
27b7c74f05
Pre Merge pull request !1369 from xy/v5-master 2025-07-30 10:52:30 +00:00
Looly
b5ccc1a7c9 add test 2025-07-24 16:26:35 +08:00
Looly
a09da9fd3d CalendarUtil增加isSameYearcalendar方法(issue#3995@Github) 2025-07-24 16:16:32 +08:00
Looly
3d0d8dea4b 修复QLExpressEngineallowClassSet无效问题(issue#3994@Github) 2025-07-22 15:26:19 +08:00
Looly
325f5a5324 add test 2025-07-21 11:29:47 +08:00
Looly
17b22a3b3d 修复TreeBuilderappend重复向idTreeMap中put问题(pr#3992@Github) 2025-07-21 11:23:54 +08:00
Golden Looly
40599b028e
Merge pull request #3992 from zhangzhiyong-ay/v5-dev
优化TreeBuilder append重复向idTreeMap中put问题
2025-07-21 11:21:37 +08:00
zhangzy
2b1bf5be59
优化TreeBuilder append重复向idTreeMap中put问题
优化TreeBuilder append重复向idTreeMap中put问题
2025-07-16 17:41:34 +08:00
Looly
acac32fd50 解决关闭时session问题 2025-07-14 16:49:03 +08:00
Looly
56e2852332 fix code 2025-07-14 10:50:56 +08:00
Looly
7188eab94c 修复ChineseDate 闰年闰月节日获取问题 2025-07-10 11:54:18 +08:00
Looly
b43899c6fb SftpreconnectIfTimeout方法改为捕获所有异常(issue#3989@Github) 2025-07-08 15:57:19 +08:00
Looly
da7d6b9d81 增加MapValueProviderRecordConverter并支持Record转换(issue#3985@Github) 2025-07-02 10:41:21 +08:00
Looly
d5bb6b2adb MathGenerator四则运算方式支持不生成负数结果(pr#1363@Gitee) 2025-07-02 09:39:10 +08:00
Looly
f632ddccb2 Merge branch 'v5-dev' of gitee.com:dromara/hutool into v5-dev 2025-07-02 09:37:03 +08:00
Looly
0c53623e0c
!1363 四则运算方式支持不生成负数结果
Merge pull request !1363 from Theo/v5-dev
2025-07-02 01:36:46 +00:00
Theo
673ab988af 四则运算方式支持不生成负数结果 2025-06-28 12:54:58 +08:00
elichow
d23b9e1f5d
Merge pull request #3982 from yangruoyu-yumeisoft/v5-dev
添加Ollama客户端支持
2025-06-27 16:34:13 +08:00
杨若瑜
d96dc0cd0a 去掉System.out.println(); 2025-06-27 15:51:43 +08:00
杨若瑜
10f6278e46 添加OllamaService的测试代码 2025-06-26 17:43:57 +08:00
Looly
d9369b1402 fix test 2025-06-26 17:21:35 +08:00
杨若瑜
53214b6fa4 添加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));

// 流式输出
aiService.chat("请帮我写一段描写Hutool的散文", System.err::println);

// 拉取模型(高耗时操作)
aiService.pullModel("qwen3:32b");
2025-06-26 01:05:34 +08:00
Looly
efb04f8a03 prepare 5.8.40 2025-06-25 10:54:17 +08:00
Looly
66f448d2fc add test 2025-06-25 10:53:05 +08:00
bugo
826665618c 🐢prepare5.8.40 2025-06-23 13:45:56 +08:00
bugo
6469e7bea6 🐢prepare 2025-06-23 12:38:46 +08:00
55 changed files with 1264 additions and 54 deletions

View File

@ -1,6 +1,20 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
# 5.8.40(2025-07-24)
### 🐣新特性
* 【captcha】 `MathGenerator`四则运算方式支持不生成负数结果pr#1363@Gitee
* 【core 】 增加`MapValueProvider`和`RecordConverter`并支持Record转换issue#3985@Github
* 【core 】 `CalendarUtil`增加`isSameYear`和`calendar`方法issue#3995@Github
### 🐞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 +39,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)

View File

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

View File

@ -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平台没有测试不能保证所有工具类或工具方法可用。

View File

@ -1 +1 @@
5.8.39
5.8.40

View File

@ -1 +1 @@
var version = '5.8.39'
var version = '5.8.40'

View File

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

View File

@ -43,7 +43,11 @@ public enum ModelName {
/**
* grok
*/
GROK("grok");
GROK("grok"),
/**
* ollama
*/
OLLAMA("ollama");
private final String value;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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