Prepare release

This commit is contained in:
Looly 2021-10-21 23:29:04 +08:00
commit 0d8dfb73d8
108 changed files with 2066 additions and 435 deletions

71
.github/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ v5-dev ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ v5-dev ]
schedule:
- cron: '45 6 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'java', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -3,6 +3,34 @@
-------------------------------------------------------------------------------------------------------------
# 5.7.15 (2021-10-21)
### 🐣新特性
* 【db 】 Db.quietSetAutoCommit增加判空issue#I4D75B@Gitee
* 【core 】 增加RingIndexUtilpr#438@Gitee
* 【core 】 Assert增加checkBetween重载pr#436@Gitee
* 【core 】 ReUtil增加命名分组重载pr#439@Gitee
* 【json 】 toString和writer增加Filterissue#I4DQNQ@Gitee
* 【core 】 ContentType增加build重载pr#1898@Github
* 【bom 】 支持scope=import方式引入issue#1561@Github
* 【core 】 新增Hash接口HashXXX继承此接口
* 【core 】 ZipUtil增加append方法pr#441@Gitee
* 【core 】 CollUtil增加重载issue#I4E9FS@Gitee
* 【core 】 CopyOptions新增setFieldValueEditorissue#I4E08T@Gitee
### 🐞Bug修复
* 【core 】 修复CollUtil.isEqualList两个null返回错误问题issue#1885@Github
* 【poi 】 修复ExcelWriter多余调试信息导致的问题issue#1884@Github
* 【poi 】 修复TemporalAccessorUtil.toInstant使用DateTimeFormatter导致问题issue#1891@Github
* 【poi 】 修复sheet.getRow(y)为null导致的问题issue#1893@Github
* 【cache 】 修复LRUCache线程安全问题issue#1895@Github
* 【crypto 】 修复KeyUtil异常信息参数丢失问题issue#1902@Github
* 【core 】 修复StrUtil.split和splittoArray不一致问题issue#I4ELU5@Github
* 【core 】 修复SymmetricCrypto未关闭CipherOutputStream导致的问题issue#I4EMST@Gitee
* 【core 】 修复QueryBuilder对/转义问题issue#1904@Github
-------------------------------------------------------------------------------------------------------------
# 5.7.14 (2021-10-09)
### 🐣新特性

View File

@ -117,6 +117,24 @@ Each module can be introduced individually, or all modules can be introduced by
-------------------------------------------------------------------------------
## 🪙Support Hutool
### 💳Donate
If you think Hutool is good, you can donate to buy the author a pack of chili~, thanks in advance ^_^.
[Gitee donate](https://gitee.com/dromara/hutool)
[Dromara donate](https://dromara.gitee.io/donate.html)
### 👕Shop about Hutool
We provide the T-Shirt and Sweater with Hutool Logo, please visit the shop
👉 [Hutool Shop](https://m.tb.cn/h.fVxoBOm?sm=2756b2) 👈
-------------------------------------------------------------------------------
## 📦Install
### 🍊Maven
@ -124,18 +142,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.7.14</version>
<version>5.7.15</version>
</dependency>
```
### 🍐Gradle
```
implementation 'cn.hutool:hutool-all:5.7.14'
implementation 'cn.hutool:hutool-all:5.7.15'
```
## 📥Download
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.14/)
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.15/)
> 🔔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.
@ -191,24 +209,6 @@ Hutool welcomes anyone to contribute code to Hutool, but the author suffers from
[![Stargazers over time](https://starchart.cc/dromara/hutool.svg)](https://starchart.cc/dromara/hutool)
## 💳Donate
If you think Hutool is good, you can donate to buy tshe author a pack of chili~, thanks in advance ^_^.
[Gitee donate](https://gitee.com/dromara/hutool)
[Dromara donate](https://dromara.gitee.io/donate.html)
## 👕shop
We provide the T-Shirt with Hutool Logo, please visit the shop
[Hutool T-Shirt](https://m.tb.cn/h.f47W8zc?sm=7d2b95)
<div align="center">
<img src="https://cdn.jsdelivr.net/gh/looly/hutool-site/images/t_shirt.jpg">
</div>
## 📌WeChat Official Account
<div align="center">

View File

@ -113,6 +113,26 @@ Hutool的存在就是为了减少代码搜索成本避免网络上参差不
-------------------------------------------------------------------------------
## 🪙支持Hutool
### 💳捐赠
如果你觉得Hutool不错可以捐赠请维护者吃包辣条~,在此表示感谢^_^。
[Gitee上捐赠](https://gitee.com/dromara/hutool)
[捐赠给Dromara组织](https://dromara.gitee.io/donate.html)
### 👕周边商店
你也可以通过购买Hutool的周边商品来支持Hutool维护哦
我们提供了印有Hutool Logo的周边商品欢迎点击购买支持
👉 [Hutool 周边商店](https://m.tb.cn/h.fVxoBOm?sm=2756b2) 👈
-------------------------------------------------------------------------------
## 📦安装
### 🍊Maven
@ -122,24 +142,24 @@ Hutool的存在就是为了减少代码搜索成本避免网络上参差不
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.14</version>
<version>5.7.15</version>
</dependency>
```
### 🍐Gradle
```
implementation 'cn.hutool:hutool-all:5.7.14'
implementation 'cn.hutool:hutool-all:5.7.15'
```
### 📥下载jar
点击以下链接,下载`hutool-all-X.X.X.jar`即可:
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.14/)
- [Maven中央库](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.7.15/)
> 🔔️注意
> Hutool 5.x支持JDK8+对Android平台没有测试不能保证所有工具类或工具方法可用。
> 如果你的项目使用JDK7请使用Hutool 4.x版本
> 如果你的项目使用JDK7请使用Hutool 4.x版本(不再更新)
### 🚽编译安装
@ -198,26 +218,6 @@ Hutool欢迎任何人为Hutool添砖加瓦贡献代码不过维护者是
[![Stargazers over time](https://starchart.cc/dromara/hutool.svg)](https://starchart.cc/dromara/hutool)
## 💳捐赠
如果你觉得Hutool不错可以捐赠请维护者吃包辣条~,在此表示感谢^_^。
点击以下链接,将页面拉到最下方点击“捐赠”即可。
[Gitee上捐赠](https://gitee.com/dromara/hutool)
[捐赠给Dromara组织](https://dromara.gitee.io/donate.html)
## 👕周边
我们提供了印有Hutool Logo的主题T恤欢迎点击购买
[HutoolT恤商店](https://m.tb.cn/h.f47W8zc?sm=7d2b95)
<div align="center">
<img src="https://cdn.jsdelivr.net/gh/looly/hutool-site/images/t_shirt.jpg">
</div>
## 📌公众号
<div align="center">

15
SECURITY.md Normal file
View File

@ -0,0 +1,15 @@
# Security Policy
## Supported Versions支持的版本
| Version | Supported |
| ------- | ------------------ |
| 5.x.x | :white_check_mark: |
| 4.x.x | :x: |
| 3.x.x | :x: |
## Reporting a Vulnerability报告漏洞
如果你发现有安全问题或漏洞,请发送邮件到`loolly@aliyun.com`。
To report any found security issues or vulnerabilities, please send a mail to `loolly@aliyun.com`.

View File

@ -1 +1 @@
5.7.14
5.7.15

View File

@ -1 +1 @@
var version = '5.7.14'
var version = '5.7.15'

View File

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

View File

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

View File

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

View File

@ -20,10 +20,10 @@ public interface BloomFilter extends Serializable{
/**
* 在boolean的bitMap中增加一个字符串<br>
* 如果存在就返回<code>false</code> .如果不存在.先增加这个字符串.再返回<code>true</code>
* 如果存在就返回{@code false} .如果不存在.先增加这个字符串.再返回{@code true}
*
* @param str 字符串
* @return 是否加入成功如果存在就返回<code>false</code> .如果不存在返回<code>true</code>
* @return 是否加入成功如果存在就返回{@code false} .如果不存在返回{@code true}
*/
boolean add(String str);
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.14</version>
<version>5.7.15</version>
</parent>
<artifactId>hutool-bom</artifactId>
@ -17,101 +17,182 @@
<description>提供丰富的Java工具方法此模块为Hutool所有模块汇总最终形式为拆分开的多个jar包可以通过exclude方式排除不需要的模块</description>
<url>https://github.com/looly/hutool</url>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-aop</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bloomFilter</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-cache</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-db</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-dfa</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-log</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-script</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-setting</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-system</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-cron</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-poi</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-socket</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-jwt</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-aop</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bloomFilter</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-cache</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-db</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-dfa</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-log</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-script</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-setting</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-system</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-cron</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-poi</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-socket</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-jwt</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>

View File

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

View File

@ -2,7 +2,6 @@ package cn.hutool.cache.impl;
import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheListener;
import cn.hutool.core.collection.CopiedIter;
import cn.hutool.core.lang.func.Func0;
import java.util.Iterator;
@ -12,7 +11,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.StampedLock;
/**
* 超时和限制大小的缓存的默认实现<br>
@ -31,11 +29,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
protected Map<K, CacheObj<K, V>> cacheMap;
// 乐观锁此处使用乐观锁解决读多写少的场景
// get时乐观读再检查是否修改修改则转入悲观读重新读一遍可以有效解决在写时阻塞大量读操作的情况
// see: https://www.cnblogs.com/jiagoushijuzi/p/13721319.html
protected final StampedLock lock = new StampedLock();
/**
* 写的时候每个key一把锁降低锁的粒度
*/
@ -75,16 +68,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
put(key, object, timeout);
}
@Override
public void put(K key, V object, long timeout) {
final long stamp = lock.writeLock();
try {
putWithoutLock(key, object, timeout);
} finally {
lock.unlockWrite(stamp);
}
}
/**
* 加入元素无锁
*
@ -93,7 +76,7 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
* @param timeout 超时时长
* @since 4.5.16
*/
private void putWithoutLock(K key, V object, long timeout) {
protected void putWithoutLock(K key, V object, long timeout) {
CacheObj<K, V> co = new CacheObj<>(key, object, timeout);
if (timeout != 0) {
existCustomTimeout = true;
@ -106,29 +89,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
// ---------------------------------------------------------------- put end
// ---------------------------------------------------------------- get start
@Override
public boolean containsKey(K key) {
final long stamp = lock.readLock();
try {
// 不存在或已移除
final CacheObj<K, V> co = cacheMap.get(key);
if (co == null) {
return false;
}
if (false == co.isExpired()) {
// 命中
return true;
}
} finally {
lock.unlockRead(stamp);
}
// 过期
remove(key, true);
return false;
}
/**
* @return 命中数
*/
@ -170,36 +130,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
}
return v;
}
@Override
public V get(K key, boolean isUpdateLastAccess) {
// 尝试读取缓存使用乐观读锁
long stamp = lock.tryOptimisticRead();
CacheObj<K, V> co = cacheMap.get(key);
if(false == lock.validate(stamp)){
// 有写线程修改了此对象悲观读
stamp = lock.readLock();
try {
co = cacheMap.get(key);
} finally {
lock.unlockRead(stamp);
}
}
// 未命中
if (null == co) {
missCount.increment();
return null;
} else if (false == co.isExpired()) {
hitCount.increment();
return co.get(isUpdateLastAccess);
}
// 过期既不算命中也不算非命中
remove(key, true);
return null;
}
// ---------------------------------------------------------------- get end
@Override
@ -207,21 +137,7 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
CacheObjIterator<K, V> copiedIterator = (CacheObjIterator<K, V>) this.cacheObjIterator();
return new CacheValuesIterator<>(copiedIterator);
}
@Override
public Iterator<CacheObj<K, V>> cacheObjIterator() {
CopiedIter<CacheObj<K, V>> copiedIterator;
final long stamp = lock.readLock();
try {
copiedIterator = CopiedIter.copyOf(this.cacheMap.values().iterator());
} finally {
lock.unlockRead(stamp);
}
return new CacheObjIterator<>(copiedIterator);
}
// ---------------------------------------------------------------- prune start
/**
* 清理实现<br>
* 子类实现此方法时无需加锁
@ -229,16 +145,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
* @return 清理数
*/
protected abstract int pruneCache();
@Override
public final int prune() {
final long stamp = lock.writeLock();
try {
return pruneCache();
} finally {
lock.unlockWrite(stamp);
}
}
// ---------------------------------------------------------------- prune end
// ---------------------------------------------------------------- common start
@ -270,21 +176,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
return (capacity > 0) && (cacheMap.size() >= capacity);
}
@Override
public void remove(K key) {
remove(key, false);
}
@Override
public void clear() {
final long stamp = lock.writeLock();
try {
cacheMap.clear();
} finally {
lock.unlockWrite(stamp);
}
}
@Override
public int size() {
return cacheMap.size();
@ -338,25 +229,6 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
}
}
/**
* 移除key对应的对象
*
* @param key
* @param withMissCount 是否计数丢失数
*/
private void remove(K key, boolean withMissCount) {
final long stamp = lock.writeLock();
CacheObj<K, V> co;
try {
co = removeWithoutLock(key, withMissCount);
} finally {
lock.unlockWrite(stamp);
}
if (null != co) {
onRemove(co.key, co.obj);
}
}
/**
* 移除key对应的对象不加锁
*
@ -364,7 +236,7 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
* @param withMissCount 是否计数丢失数
* @return 移除的对象无返回null
*/
private CacheObj<K, V> removeWithoutLock(K key, boolean withMissCount) {
protected CacheObj<K, V> removeWithoutLock(K key, boolean withMissCount) {
final CacheObj<K, V> co = cacheMap.remove(key);
if (withMissCount) {
// 在丢失计数有效的情况下移除一般为get时的超时操作此处应该丢失数+1

View File

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

View File

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

View File

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

View File

@ -0,0 +1,136 @@
package cn.hutool.cache.impl;
import cn.hutool.core.collection.CopiedIter;
import java.util.Iterator;
import java.util.concurrent.locks.ReentrantLock;
/**
* 使用{@link ReentrantLock}保护的缓存读写都使用悲观锁完成主要避免某些Map无法使用读写锁的问题<br>
* 例如使用了LinkedHashMap的缓存由于get方法也会改变Map的结构因此读写必须加互斥锁
*
* @param <K> 键类型
* @param <V> 值类型
* @author looly
* @since 5.7.15
*/
public abstract class ReentrantCache<K, V> extends AbstractCache<K, V> {
private static final long serialVersionUID = 1L;
// 一些特殊缓存例如使用了LinkedHashMap的缓存由于get方法也会改变Map的结构导致无法使用读写锁
// 最优的解决方案是使用Guava的ConcurrentLinkedHashMap此处使用简化的互斥锁
protected final ReentrantLock lock = new ReentrantLock();
@Override
public void put(K key, V object, long timeout) {
lock.lock();
try {
putWithoutLock(key, object, timeout);
} finally {
lock.unlock();
}
}
@Override
public boolean containsKey(K key) {
lock.lock();
try {
// 不存在或已移除
final CacheObj<K, V> co = cacheMap.get(key);
if (co == null) {
return false;
}
if (false == co.isExpired()) {
// 命中
return true;
}
} finally {
lock.unlock();
}
// 过期
remove(key, true);
return false;
}
@Override
public V get(K key, boolean isUpdateLastAccess) {
CacheObj<K, V> co;
lock.lock();
try {
co = cacheMap.get(key);
} finally {
lock.unlock();
}
// 未命中
if (null == co) {
missCount.increment();
return null;
} else if (false == co.isExpired()) {
hitCount.increment();
return co.get(isUpdateLastAccess);
}
// 过期既不算命中也不算非命中
remove(key, true);
return null;
}
@Override
public Iterator<CacheObj<K, V>> cacheObjIterator() {
CopiedIter<CacheObj<K, V>> copiedIterator;
lock.lock();
try {
copiedIterator = CopiedIter.copyOf(this.cacheMap.values().iterator());
} finally {
lock.unlock();
}
return new CacheObjIterator<>(copiedIterator);
}
@Override
public final int prune() {
lock.lock();
try {
return pruneCache();
} finally {
lock.unlock();
}
}
@Override
public void remove(K key) {
remove(key, false);
}
@Override
public void clear() {
lock.lock();
try {
cacheMap.clear();
} finally {
lock.unlock();
}
}
/**
* 移除key对应的对象
*
* @param key
* @param withMissCount 是否计数丢失数
*/
private void remove(K key, boolean withMissCount) {
lock.lock();
CacheObj<K, V> co;
try {
co = removeWithoutLock(key, withMissCount);
} finally {
lock.unlock();
}
if (null != co) {
onRemove(co.key, co.obj);
}
}
}

View File

@ -0,0 +1,141 @@
package cn.hutool.cache.impl;
import cn.hutool.core.collection.CopiedIter;
import java.util.Iterator;
import java.util.concurrent.locks.StampedLock;
/**
* 使用{@link StampedLock}保护的缓存使用读写乐观锁
*
* @param <K> 键类型
* @param <V> 值类型
* @author looly
* @since 5.7.15
*/
public abstract class StampedCache<K, V> extends AbstractCache<K, V>{
private static final long serialVersionUID = 1L;
// 乐观锁此处使用乐观锁解决读多写少的场景
// get时乐观读再检查是否修改修改则转入悲观读重新读一遍可以有效解决在写时阻塞大量读操作的情况
// see: https://www.cnblogs.com/jiagoushijuzi/p/13721319.html
protected final StampedLock lock = new StampedLock();
@Override
public void put(K key, V object, long timeout) {
final long stamp = lock.writeLock();
try {
putWithoutLock(key, object, timeout);
} finally {
lock.unlockWrite(stamp);
}
}
@Override
public boolean containsKey(K key) {
final long stamp = lock.readLock();
try {
// 不存在或已移除
final CacheObj<K, V> co = cacheMap.get(key);
if (co == null) {
return false;
}
if (false == co.isExpired()) {
// 命中
return true;
}
} finally {
lock.unlockRead(stamp);
}
// 过期
remove(key, true);
return false;
}
@Override
public V get(K key, boolean isUpdateLastAccess) {
// 尝试读取缓存使用乐观读锁
long stamp = lock.tryOptimisticRead();
CacheObj<K, V> co = cacheMap.get(key);
if(false == lock.validate(stamp)){
// 有写线程修改了此对象悲观读
stamp = lock.readLock();
try {
co = cacheMap.get(key);
} finally {
lock.unlockRead(stamp);
}
}
// 未命中
if (null == co) {
missCount.increment();
return null;
} else if (false == co.isExpired()) {
hitCount.increment();
return co.get(isUpdateLastAccess);
}
// 过期既不算命中也不算非命中
remove(key, true);
return null;
}
@Override
public Iterator<CacheObj<K, V>> cacheObjIterator() {
CopiedIter<CacheObj<K, V>> copiedIterator;
final long stamp = lock.readLock();
try {
copiedIterator = CopiedIter.copyOf(this.cacheMap.values().iterator());
} finally {
lock.unlockRead(stamp);
}
return new CacheObjIterator<>(copiedIterator);
}
@Override
public final int prune() {
final long stamp = lock.writeLock();
try {
return pruneCache();
} finally {
lock.unlockWrite(stamp);
}
}
@Override
public void remove(K key) {
remove(key, false);
}
@Override
public void clear() {
final long stamp = lock.writeLock();
try {
cacheMap.clear();
} finally {
lock.unlockWrite(stamp);
}
}
/**
* 移除key对应的对象
*
* @param key
* @param withMissCount 是否计数丢失数
*/
private void remove(K key, boolean withMissCount) {
final long stamp = lock.writeLock();
CacheObj<K, V> co;
try {
co = removeWithoutLock(key, withMissCount);
} finally {
lock.unlockWrite(stamp);
}
if (null != co) {
onRemove(co.key, co.obj);
}
}
}

View File

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

View File

@ -0,0 +1,52 @@
package cn.hutool.cache;
import cn.hutool.cache.impl.LRUCache;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
/**
* https://github.com/dromara/hutool/issues/1895<br>
* 并发问题测试在5.7.15前LRUCache存在并发问题多线程get后map结构变更导致null的位置不确定
* 并可能引起死锁
*/
public class LRUCacheTest {
@Test
public void readWriteTest() throws InterruptedException {
LRUCache<Integer, Integer> cache = CacheUtil.newLRUCache(10);
for (int i = 0; i < 10; i++) {
cache.put(i, i);
}
CountDownLatch countDownLatch = new CountDownLatch(10);
// 10个线程分别读0-9 10000次
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
cache.get(finalI);
}
countDownLatch.countDown();
}).start();
}
// 等待读线程结束
countDownLatch.await();
// 按顺序读0-9
StringBuilder sb1 = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb1.append(cache.get(i));
}
Assert.assertEquals("0123456789", sb1.toString());
// 新加11此时0最久未使用应该淘汰0
cache.put(11, 11);
StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb2.append(cache.get(i));
}
Assert.assertEquals("null123456789", sb2.toString());
}
}

View File

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

View File

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

View File

@ -664,6 +664,32 @@ public class BeanUtil {
).copy();
}
/**
* 对象转Map<br>
* 通过自定义{@link CopyOptions} 完成抓换选项以便实现
*
* <pre>
* 1. 字段筛选可以去除不需要的字段
* 2. 字段变换例如实现驼峰转下划线
* 3. 自定义字段前缀或后缀等等
* 4. 字段值处理
* ...
* </pre>
*
* @param bean bean对象
* @param targetMap 目标的Map
* @param copyOptions 拷贝选项
* @return Map
* @since 5.7.15
*/
public static Map<String, Object> beanToMap(Object bean, Map<String, Object> targetMap, CopyOptions copyOptions) {
if (null == bean) {
return null;
}
return BeanCopier.create(bean, targetMap, copyOptions).copy();
}
// --------------------------------------------------------------------------------------------- copyProperties
/**

View File

@ -191,6 +191,10 @@ public class BeanCopier<T> implements Copier<T>, Serializable {
if(null != copyOptions.propertiesFilter && false == copyOptions.propertiesFilter.test(prop.getField(), value)) {
return;
}
// since 5.7.15
value = copyOptions.editFieldValue(key, value);
if ((null == value && copyOptions.ignoreNullValue) || bean == value) {
// 当允许跳过空时跳过
//值不能为bean本身防止循环引用此类也跳过
@ -257,6 +261,9 @@ public class BeanCopier<T> implements Copier<T>, Serializable {
return;
}
// since 5.7.15
value = copyOptions.editFieldValue(providerKey, value);
if ((null == value && copyOptions.ignoreNullValue) || bean == value) {
// 当允许跳过空时跳过
// 值不能为bean本身防止循环引用

View File

@ -7,6 +7,7 @@ import cn.hutool.core.util.ObjectUtil;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
/**
@ -57,6 +58,10 @@ public class CopyOptions implements Serializable {
* 字段属性编辑器用于自定义属性转换规则例如驼峰转下划线等
*/
protected Editor<String> fieldNameEditor;
/**
* 字段属性值编辑器用于自定义属性值转换规则例如null转""
*/
protected BiFunction<String, Object, Object> fieldValueEditor;
/**
* 是否支持transient关键字修饰和@Transient注解如果支持被修饰的字段或方法对应的字段将被忽略
*/
@ -224,6 +229,30 @@ public class CopyOptions implements Serializable {
return this;
}
/**
* 设置字段属性值编辑器用于自定义属性值转换规则例如null转""<br>
*
* @param fieldValueEditor 字段属性值编辑器用于自定义属性值转换规则例如null转""
* @return CopyOptions
* @since 5.7.15
*/
public CopyOptions setFieldValueEditor(BiFunction<String, Object, Object> fieldValueEditor) {
this.fieldValueEditor = fieldValueEditor;
return this;
}
/**
* 转换字段名为编辑后的字段名
*
* @param fieldName 字段名
* @return 编辑后的字段名
* @since 5.7.15
*/
protected Object editFieldValue(String fieldName, Object fieldValue) {
return (null != this.fieldValueEditor) ?
this.fieldValueEditor.apply(fieldName, fieldValue) : fieldValue;
}
/**
* 是否支持transient关键字修饰和@Transient注解如果支持被修饰的字段或方法对应的字段将被忽略
*
@ -251,12 +280,12 @@ public class CopyOptions implements Serializable {
* 当非反向则根据源字段名获取目标字段名反之根据目标字段名获取源字段名
*
* @param fieldName 字段名
* @param reversed 是否反向映射
* @param reversed 是否反向映射
* @return 映射后的字段名
*/
protected String getMappedFieldName(String fieldName, boolean reversed){
protected String getMappedFieldName(String fieldName, boolean reversed) {
Map<String, String> mapping = reversed ? getReversedMapping() : this.fieldMapping;
if(MapUtil.isEmpty(mapping)){
if (MapUtil.isEmpty(mapping)) {
return fieldName;
}
return ObjectUtil.defaultIfNull(mapping.get(fieldName), fieldName);
@ -264,11 +293,12 @@ public class CopyOptions implements Serializable {
/**
* 转换字段名为编辑后的字段名
*
* @param fieldName 字段名
* @return 编辑后的字段名
* @since 5.4.2
*/
protected String editFieldName(String fieldName){
protected String editFieldName(String fieldName) {
return (null != this.fieldNameEditor) ? this.fieldNameEditor.edit(fieldName) : fieldName;
}
@ -279,10 +309,10 @@ public class CopyOptions implements Serializable {
* @since 4.1.10
*/
private Map<String, String> getReversedMapping() {
if(null == this.fieldMapping){
if (null == this.fieldMapping) {
return null;
}
if(null == this.reversedFieldMapping){
if (null == this.reversedFieldMapping) {
reversedFieldMapping = MapUtil.reverse(this.fieldMapping);
}
return reversedFieldMapping;

View File

@ -299,7 +299,7 @@ public class Base64 {
* base64解码
*
* @param base64 被解码的base64字符串
* @return 被加密后的字符串
* @return 解码后的bytes
*/
public static byte[] decode(CharSequence base64) {
return Base64Decoder.decode(base64);

View File

@ -53,6 +53,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* 集合相关工具类
@ -1050,8 +1051,8 @@ public class CollUtil {
* @param end 结束位置不包含
* @param step 步进
* @return 截取后的数组当开始位置超过最大时返回空的List
* @since 4.0.6
* @see ListUtil#sub(List, int, int, int)
* @since 4.0.6
*/
public static <T> List<T> sub(List<T> list, int start, int end, int step) {
return ListUtil.sub(list, start, end, step);
@ -1073,11 +1074,11 @@ public class CollUtil {
/**
* 截取集合的部分
*
* @param <T> 集合元素类型
* @param collection 被截取的数组
* @param start 开始位置包含
* @param end 结束位置不包含
* @param step 步进
* @param <T> 集合元素类型
* @param collection 被截取的数组
* @param start 开始位置包含
* @param end 结束位置不包含
* @param step 步进
* @return 截取后的数组当开始位置超过最大时返回空集合
* @since 4.0.6
*/
@ -1086,7 +1087,7 @@ public class CollUtil {
return ListUtil.empty();
}
final List<T> list = collection instanceof List ? (List<T>)collection : ListUtil.toList(collection);
final List<T> list = collection instanceof List ? (List<T>) collection : ListUtil.toList(collection);
return sub(list, start, end, step);
}
@ -1575,6 +1576,20 @@ public class CollUtil {
return isEmpty(collection) ? defaultCollection : collection;
}
/**
* 如果给定集合为空返回默认集合
*
* @param <T> 集合类型
* @param <E> 集合元素类型
* @param collection 集合
* @param supplier 默认值懒加载函数
* @return 非空empty的原集合或默认集合
* @since 5.7.15
*/
public static <T extends Collection<E>, E> T defaultIfEmpty(T collection, Supplier<? extends T> supplier) {
return isEmpty(collection) ? supplier.get() : collection;
}
/**
* Iterable是否为空
*
@ -2948,6 +2963,9 @@ public class CollUtil {
* @since 5.6.0
*/
public static boolean isEqualList(final Collection<?> list1, final Collection<?> list2) {
if (list1 == list2) {
return true;
}
if (list1 == null || list2 == null || list1.size() != list2.size()) {
return false;
}

View File

@ -819,7 +819,7 @@ public class IterUtil {
* @return Iterable对象的元素数量
* @since 5.5.0
*/
public static int size(final Iterable<?> iterable) {
public static int size(Iterable<?> iterable) {
if (null == iterable) {
return 0;
}
@ -838,7 +838,7 @@ public class IterUtil {
* @return Iterator对象的元素数量
* @since 5.5.0
*/
public static int size(final Iterator<?> iterator) {
public static int size(Iterator<?> iterator) {
int size = 0;
if (iterator != null) {
while (iterator.hasNext()) {
@ -862,7 +862,7 @@ public class IterUtil {
* @return 是否相同
* @since 5.6.0
*/
public static boolean isEqualList(final Iterable<?> list1, final Iterable<?> list2) {
public static boolean isEqualList(Iterable<?> list1, Iterable<?> list2) {
if (list1 == list2) {
return true;
}

View File

@ -0,0 +1,79 @@
package cn.hutool.core.collection;
import cn.hutool.core.lang.Assert;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* 集合索引环形获取工具类
*
* @author ZhouChuGang
* @since 5.7.15
*/
public class RingIndexUtil {
/**
* 通过cas操作 实现对指定值内的回环累加
*
* @param object 集合
* <ul>
* <li>Collection - the collection size
* <li>Map - the map size
* <li>Array - the array size
* <li>Iterator - the number of elements remaining in the iterator
* <li>Enumeration - the number of elements remaining in the enumeration
* </ul>
* @param atomicInteger 原子操作类
* @return 索引位置
*/
public static int ringNextIntByObj(Object object, AtomicInteger atomicInteger) {
Assert.notNull(object);
int modulo = CollUtil.size(object);
return ringNextInt(modulo, atomicInteger);
}
/**
* 通过cas操作 实现对指定值内的回环累加
*
* @param modulo 回环周期值
* @param atomicInteger 原子操作类
* @return 索引位置
*/
public static int ringNextInt(int modulo, AtomicInteger atomicInteger) {
Assert.notNull(atomicInteger);
Assert.isTrue(modulo > 0);
if (modulo <= 1) {
return 0;
}
for (; ; ) {
int current = atomicInteger.get();
int next = (current + 1) % modulo;
if (atomicInteger.compareAndSet(current, next)) {
return next;
}
}
}
/**
* 通过cas操作 实现对指定值内的回环累加
*
* @param modulo 回环周期值
* @param atomicLong 原子操作类
* @return 索引位置
*/
public static long ringNextLong(long modulo, AtomicLong atomicLong) {
Assert.notNull(atomicLong);
Assert.isTrue(modulo > 0);
if (modulo <= 1) {
return 0;
}
for (; ; ) {
long current = atomicLong.get();
long next = (current + 1) % modulo;
if (atomicLong.compareAndSet(current, next)) {
return next;
}
}
}
}

View File

@ -0,0 +1,84 @@
package cn.hutool.core.compress;
import cn.hutool.core.util.StrUtil;
import java.io.IOException;
import java.nio.file.CopyOption;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
/**
* Zip文件拷贝的FileVisitor实现zip中追加文件此类非线程安全<br>
* 此类在遍历源目录并复制过程中会自动创建目标目录中不存在的上级目录
*
* @author looly
* @since 5.7.15
*/
public class ZipCopyVisitor extends SimpleFileVisitor<Path> {
/**
* 源Path或基准路径用于计算被拷贝文件的相对路径
*/
private final Path source;
private final FileSystem fileSystem;
private final CopyOption[] copyOptions;
/**
* 构造
*
* @param source 源Path或基准路径用于计算被拷贝文件的相对路径
* @param fileSystem 目标Zip文件
* @param copyOptions 拷贝选项如跳过已存在等
*/
public ZipCopyVisitor(Path source, FileSystem fileSystem, CopyOption... copyOptions) {
this.source = source;
this.fileSystem = fileSystem;
this.copyOptions = copyOptions;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
final Path targetDir = resolveTarget(dir);
if(StrUtil.isNotEmpty(targetDir.toString())){
// 在目标的Zip文件中的相对位置创建目录
try {
Files.copy(dir, targetDir, copyOptions);
} catch (FileAlreadyExistsException e) {
if (false == Files.isDirectory(targetDir)) {
throw e;
}
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
// 如果目标存在无论目录还是文件都抛出FileAlreadyExistsException异常此处不做特别处理
Files.copy(file, resolveTarget(file), copyOptions);
return FileVisitResult.CONTINUE;
}
/**
* 根据源文件或目录路径拼接生成目标的文件或目录路径<br>
* 原理是首先截取源路径得到相对路径再和目标路径拼接
*
* <p>
* 源路径是 /opt/test/需要拷贝的文件是 /opt/test/a/a.txt得到相对路径 a/a.txt<br>
* 目标路径是/home/则得到最终目标路径是 /home/a/a.txt
* </p>
*
* @param file 需要拷贝的文件或目录Path
* @return 目标Path
*/
private Path resolveTarget(Path file) {
return fileSystem.getPath(source.relativize(file).toString());
}
}

View File

@ -26,22 +26,22 @@ import java.util.zip.ZipOutputStream;
public class ZipWriter implements Closeable {
/**
* 创建{@link ZipWriter}
* 创建ZipWriter
*
* @param zipFile 生成的Zip文件
* @param charset 编码
* @return {@link ZipWriter}
* @return ZipWriter
*/
public static ZipWriter of(File zipFile, Charset charset) {
return new ZipWriter(zipFile, charset);
}
/**
* 创建{@link ZipWriter}
* 创建ZipWriter
*
* @param out Zip输出的流一般为输出文件流
* @param charset 编码
* @return {@link ZipWriter}
* @return ZipWriter
*/
public static ZipWriter of(OutputStream out, Charset charset) {
return new ZipWriter(out, charset);

View File

@ -167,7 +167,7 @@ public class DateTime extends Date {
* @since 5.0.5
*/
public DateTime(Instant instant, ZoneId zoneId) {
this(instant.toEpochMilli(), TimeZone.getTimeZone(ObjectUtil.defaultIfNull(zoneId, ZoneId.systemDefault())));
this(instant.toEpochMilli(), ZoneUtil.toTimeZone(zoneId));
}
/**
@ -177,7 +177,7 @@ public class DateTime extends Date {
* @since 5.0.0
*/
public DateTime(TemporalAccessor temporalAccessor) {
this(DateUtil.toInstant(temporalAccessor));
this(TemporalAccessorUtil.toInstant(temporalAccessor));
}
/**
@ -276,7 +276,7 @@ public class DateTime extends Date {
* @since 5.0.0
*/
public DateTime(CharSequence dateStr, DateTimeFormatter formatter) {
this(Instant.from(formatter.parse(dateStr)), formatter.getZone());
this(TemporalAccessorUtil.toInstant(formatter.parse(dateStr)), formatter.getZone());
}
/**

View File

@ -138,7 +138,10 @@ public class TemporalAccessorUtil extends TemporalUtil{
// 指定本地时间转换 为Instant取当天日期
result = ((OffsetTime) temporalAccessor).atDate(LocalDate.now()).toInstant();
} else {
result = Instant.from(temporalAccessor);
// issue#1891@Github
// Instant.from不能完成日期转换
//result = Instant.from(temporalAccessor);
result = toInstant(LocalDateTimeUtil.of(temporalAccessor));
}
return result;

View File

@ -0,0 +1,41 @@
package cn.hutool.core.date;
import java.time.ZoneId;
import java.util.TimeZone;
/**
* {@link ZoneId}{@link TimeZone}相关封装
*
* @author looly
* @since 5.7.15
*/
public class ZoneUtil {
/**
* {@link ZoneId}转换为{@link TimeZone}{@code null}则返回系统默认值
*
* @param zoneId {@link ZoneId}{@code null}则返回系统默认值
* @return {@link TimeZone}
*/
public static TimeZone toTimeZone(ZoneId zoneId) {
if (null == zoneId) {
return TimeZone.getDefault();
}
return TimeZone.getTimeZone(zoneId);
}
/**
* {@link TimeZone}转换为{@link ZoneId}{@code null}则返回系统默认值
*
* @param timeZone {@link TimeZone}{@code null}则返回系统默认值
* @return {@link ZoneId}
*/
public static ZoneId toZoneId(TimeZone timeZone) {
if (null == timeZone) {
return ZoneId.systemDefault();
}
return timeZone.toZoneId();
}
}

View File

@ -684,7 +684,7 @@ public class IoUtil extends NioUtil {
* @return 内容
* @throws IORuntimeException IO异常
*/
public static <T extends Collection<String>> T readLines(Reader reader, final T collection) throws IORuntimeException {
public static <T extends Collection<String>> T readLines(Reader reader, T collection) throws IORuntimeException {
readLines(reader, (LineHandler) collection::add);
return collection;
}

View File

@ -0,0 +1,84 @@
package cn.hutool.core.io.file;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
/**
* {@link FileSystem}相关工具类封装<br>
* 参考https://blog.csdn.net/j16421881/article/details/78858690
*
* @author looly
* @since 5.7.15
*/
public class FileSystemUtil {
/**
* 创建 {@link FileSystem}
*
* @param path 文件路径可以是目录或Zip文件等
* @return {@link FileSystem}
*/
public static FileSystem create(String path) {
try {
return FileSystems.newFileSystem(
Paths.get(path).toUri(),
MapUtil.of("create", "true"));
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 创建 Zip的{@link FileSystem}默认UTF-8编码
*
* @param path 文件路径可以是目录或Zip文件等
* @return {@link FileSystem}
*/
public static FileSystem createZip(String path) {
return createZip(path, null);
}
/**
* 创建 Zip的{@link FileSystem}
*
* @param path 文件路径可以是目录或Zip文件等
* @param charset 编码
* @return {@link FileSystem}
*/
public static FileSystem createZip(String path, Charset charset) {
if(null == charset){
charset = CharsetUtil.CHARSET_UTF_8;
}
final HashMap<String, String> env = new HashMap<>();
env.put("create", "true");
env.put("encoding", charset.name());
try {
return FileSystems.newFileSystem(
URI.create("jar:" + Paths.get(path).toUri()), env);
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 获取目录的根路径或Zip文件中的根路径
*
* @param fileSystem {@link FileSystem}
* @return {@link Path}
*/
public static Path getRoot(FileSystem fileSystem) {
return fileSystem.getPath(StrUtil.SLASH);
}
}

View File

@ -55,6 +55,19 @@ public class PathUtil {
}
}
/**
* 递归遍历目录以及子目录中的所有文件<br>
* 如果提供path为文件直接返回过滤结果
*
* @param path 当前遍历文件或目录
* @param fileFilter 文件过滤规则对象选择要保留的文件只对文件有效不过滤目录null表示接收全部文件
* @return 文件列表
* @since 5.4.1
*/
public static List<File> loopFiles(Path path, FileFilter fileFilter) {
return loopFiles(path, -1, fileFilter);
}
/**
* 递归遍历目录以及子目录中的所有文件<br>
* 如果提供path为文件直接返回过滤结果
@ -643,6 +656,20 @@ public class PathUtil {
return mkdir(path.getParent());
}
/**
* 获取{@link Path}文件名
*
* @param path {@link Path}
* @return 文件名
* @since 5.7.15
*/
public static String getName(Path path) {
if (null == path) {
return null;
}
return path.getFileName().toString();
}
/**
* 删除文件或空目录不追踪软链
*

View File

@ -20,20 +20,27 @@ import java.nio.file.attribute.BasicFileAttributes;
*/
public class CopyVisitor extends SimpleFileVisitor<Path> {
/**
* 源Path或基准路径用于计算被拷贝文件的相对路径
*/
private final Path source;
private final Path target;
private boolean isTargetCreated;
private final CopyOption[] copyOptions;
/**
* 标记目标目录是否创建省略每次判断目标是否存在
*/
private boolean isTargetCreated;
/**
* 构造
*
* @param source 源Path
* @param target 目标Path
* @param source 源Path或基准路径用于计算被拷贝文件的相对路径
* @param target 目标Path
* @param copyOptions 拷贝选项如跳过已存在等
*/
public CopyVisitor(Path source, Path target, CopyOption... copyOptions) {
if(PathUtil.exists(target, false) && false == PathUtil.isDirectory(target)){
if (PathUtil.exists(target, false) && false == PathUtil.isDirectory(target)) {
throw new IllegalArgumentException("Target must be a directory");
}
this.source = source;
@ -42,16 +49,19 @@ public class CopyVisitor extends SimpleFileVisitor<Path> {
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
initTarget();
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
initTargetDir();
// 将当前目录相对于源路径转换为相对于目标路径
final Path targetDir = target.resolve(source.relativize(dir));
final Path targetDir = resolveTarget(dir);
// 在目录不存在的情况下copy方法会创建新目录
try {
Files.copy(dir, targetDir, copyOptions);
} catch (FileAlreadyExistsException e) {
if (false == Files.isDirectory(targetDir))
if (false == Files.isDirectory(targetDir)) {
// 目标文件存在抛出异常目录忽略
throw e;
}
}
return FileVisitResult.CONTINUE;
}
@ -59,16 +69,33 @@ public class CopyVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
initTarget();
Files.copy(file, target.resolve(source.relativize(file)), copyOptions);
initTargetDir();
// 如果目标存在无论目录还是文件都抛出FileAlreadyExistsException异常此处不做特别处理
Files.copy(file, resolveTarget(file), copyOptions);
return FileVisitResult.CONTINUE;
}
/**
* 根据源文件或目录路径拼接生成目标的文件或目录路径<br>
* 原理是首先截取源路径得到相对路径再和目标路径拼接
*
* <p>
* 源路径是 /opt/test/需要拷贝的文件是 /opt/test/a/a.txt得到相对路径 a/a.txt<br>
* 目标路径是/home/则得到最终目标路径是 /home/a/a.txt
* </p>
*
* @param file 需要拷贝的文件或目录Path
* @return 目标Path
*/
private Path resolveTarget(Path file) {
return target.resolve(source.relativize(file));
}
/**
* 初始化目标文件或目录
*/
private void initTarget(){
if(false == this.isTargetCreated){
private void initTargetDir() {
if (false == this.isTargetCreated) {
PathUtil.mkdir(this.target);
this.isTargetCreated = true;
}

View File

@ -16,6 +16,9 @@ import java.util.function.Supplier;
*/
public class Assert {
private static final String TEMPLATE_VALUE_MUST_BE_BETWEEN_AND = "The value must be between {} and {}.";
/**
* 断言是否为真如果为 {@code false} 抛出给定的异常<br>
*
@ -834,6 +837,38 @@ public class Assert {
return index;
}
/**
* 检查值是否在指定范围内
*
* @param value
* @param min 最小值包含
* @param max 最大值包含
* @param errorSupplier 错误抛出异常附带的消息生产接口
* @return 经过检查后的值
* @throws X if value is out of bound
* @since 5.7.15
*/
public static <X extends Throwable> int checkBetween(int value, int min, int max, Supplier<? extends X> errorSupplier) throws X {
if (value < min || value > max) {
throw errorSupplier.get();
}
return value;
}
/**
* 检查值是否在指定范围内
*
* @param value
* @param min 最小值包含
* @param max 最大值包含
* @return 经过检查后的值
* @since 5.7.15
*/
public static int checkBetween(int value, int min, int max, String errorMsgTemplate, Object... params) {
return checkBetween(value, min, max, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));
}
/**
* 检查值是否在指定范围内
*
@ -844,12 +879,41 @@ public class Assert {
* @since 4.1.10
*/
public static int checkBetween(int value, int min, int max) {
return checkBetween(value, min, max, TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max);
}
/**
* 检查值是否在指定范围内
*
* @param value
* @param min 最小值包含
* @param max 最大值包含
* @param errorSupplier 错误抛出异常附带的消息生产接口
* @return 经过检查后的值
* @throws X if value is out of bound
* @since 5.7.15
*/
public static <X extends Throwable> long checkBetween(long value, long min, long max, Supplier<? extends X> errorSupplier) throws X {
if (value < min || value > max) {
throw new IllegalArgumentException(StrUtil.format("Length must be between {} and {}.", min, max));
throw errorSupplier.get();
}
return value;
}
/**
* 检查值是否在指定范围内
*
* @param value
* @param min 最小值包含
* @param max 最大值包含
* @return 经过检查后的值
* @since 5.7.15
*/
public static long checkBetween(long value, long min, long max, String errorMsgTemplate, Object... params) {
return checkBetween(value, min, max, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));
}
/**
* 检查值是否在指定范围内
*
@ -860,12 +924,41 @@ public class Assert {
* @since 4.1.10
*/
public static long checkBetween(long value, long min, long max) {
return checkBetween(value, min, max, TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max);
}
/**
* 检查值是否在指定范围内
*
* @param value
* @param min 最小值包含
* @param max 最大值包含
* @param errorSupplier 错误抛出异常附带的消息生产接口
* @return 经过检查后的值
* @throws X if value is out of bound
* @since 5.7.15
*/
public static <X extends Throwable> double checkBetween(double value, double min, double max, Supplier<? extends X> errorSupplier) throws X {
if (value < min || value > max) {
throw new IllegalArgumentException(StrUtil.format("Length must be between {} and {}.", min, max));
throw errorSupplier.get();
}
return value;
}
/**
* 检查值是否在指定范围内
*
* @param value
* @param min 最小值包含
* @param max 最大值包含
* @return 经过检查后的值
* @since 5.7.15
*/
public static double checkBetween(double value, double min, double max, String errorMsgTemplate, Object... params) {
return checkBetween(value, min, max, () -> new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)));
}
/**
* 检查值是否在指定范围内
*
@ -876,10 +969,7 @@ public class Assert {
* @since 4.1.10
*/
public static double checkBetween(double value, double min, double max) {
if (value < min || value > max) {
throw new IllegalArgumentException(StrUtil.format("Length must be between {} and {}.", min, max));
}
return value;
return checkBetween(value, min, max, TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max);
}
/**
@ -899,7 +989,7 @@ public class Assert {
double minDouble = min.doubleValue();
double maxDouble = max.doubleValue();
if (valueDouble < minDouble || valueDouble > maxDouble) {
throw new IllegalArgumentException(StrUtil.format("Length must be between {} and {}.", min, max));
throw new IllegalArgumentException(StrUtil.format(TEMPLATE_VALUE_MUST_BE_BETWEEN_AND, min, max));
}
return value;
}

View File

@ -318,7 +318,7 @@ public class CityHash {
private static long hashLen0to16(byte[] byteArray) {
int len = byteArray.length;
if (len >= 8) {
long mul = k2 + len * 2;
long mul = k2 + len * 2L;
long a = fetch64(byteArray, 0) + k2;
long b = fetch64(byteArray, len - 8);
long c = rotate(b, 37) * mul + a;
@ -344,7 +344,7 @@ public class CityHash {
// This probably works well for 16-byte strings as well, but it may be overkill in that case.
private static long hashLen17to32(byte[] byteArray) {
int len = byteArray.length;
long mul = k2 + len * 2;
long mul = k2 + len * 2L;
long a = fetch64(byteArray, 0) * k1;
long b = fetch64(byteArray, 8);
long c = fetch64(byteArray, len - 8) * mul;
@ -355,7 +355,7 @@ public class CityHash {
private static long hashLen33to64(byte[] byteArray) {
int len = byteArray.length;
long mul = k2 + len * 2;
long mul = k2 + len * 2L;
long a = fetch64(byteArray, 0) * k2;
long b = fetch64(byteArray, 8);
long c = fetch64(byteArray, len - 24);

View File

@ -0,0 +1,19 @@
package cn.hutool.core.lang.hash;
/**
* Hash计算接口
*
* @param <T> 被计算hash的对象类型
* @author looly
* @since 5.7.15
*/
@FunctionalInterface
public interface Hash<T> {
/**
* 计算Hash值
*
* @param t 对象
* @return hash
*/
Number hash(T t);
}

View File

@ -8,7 +8,8 @@ package cn.hutool.core.lang.hash;
* @since 5.2.5
*/
@FunctionalInterface
public interface Hash128<T> {
public interface Hash128<T> extends Hash<T>{
/**
* 计算Hash值
*
@ -16,4 +17,9 @@ public interface Hash128<T> {
* @return hash
*/
Number128 hash128(T t);
}
@Override
default Number hash(T t){
return hash128(t);
}
}

View File

@ -8,7 +8,7 @@ package cn.hutool.core.lang.hash;
* @since 5.2.5
*/
@FunctionalInterface
public interface Hash32<T> {
public interface Hash32<T> extends Hash<T>{
/**
* 计算Hash值
*
@ -16,4 +16,9 @@ public interface Hash32<T> {
* @return hash
*/
int hash32(T t);
}
@Override
default Number hash(T t){
return hash32(t);
}
}

View File

@ -8,7 +8,7 @@ package cn.hutool.core.lang.hash;
* @since 5.2.5
*/
@FunctionalInterface
public interface Hash64<T> {
public interface Hash64<T> extends Hash<T>{
/**
* 计算Hash值
*
@ -16,4 +16,9 @@ public interface Hash64<T> {
* @return hash
*/
long hash64(T t);
}
@Override
default Number hash(T t){
return hash64(t);
}
}

View File

@ -6,7 +6,8 @@ package cn.hutool.core.lang.hash;
* @author hexiufeng
* @since 5.2.5
*/
public class Number128 {
public class Number128 extends Number{
private static final long serialVersionUID = 1L;
private long lowValue;
private long highValue;
@ -41,4 +42,24 @@ public class Number128 {
public long[] getLongArray() {
return new long[]{lowValue, highValue};
}
@Override
public int intValue() {
return (int) longValue();
}
@Override
public long longValue() {
return this.lowValue;
}
@Override
public float floatValue() {
return longValue();
}
@Override
public double doubleValue() {
return longValue();
}
}

View File

@ -3,7 +3,7 @@ package cn.hutool.core.lang.mutable;
import java.io.Serializable;
/**
* 可变 <code>boolean</code> 类型
* 可变 {@code boolean} 类型
*
* @see Boolean
* @since 3.0.1
@ -59,12 +59,12 @@ public class MutableBool implements Comparable<MutableBool>, Mutable<Boolean>, S
* 相等需同时满足如下条件
* <ol>
* <li>非空</li>
* <li>类型为 {@link MutableBool}</li>
* <li>类型为 MutableBool</li>
* <li>值相等</li>
* </ol>
*
* @param obj 比对的对象
* @return 相同返回<code>true</code>否则 <code>false</code>
* @return 相同返回<code>true</code>否则 {@code false}
*/
@Override
public boolean equals(final Object obj) {
@ -83,7 +83,7 @@ public class MutableBool implements Comparable<MutableBool>, Mutable<Boolean>, S
/**
* 比较
*
* @param other 其它 {@link MutableBool} 对象
* @param other 其它 MutableBool 对象
* @return x==y返回0x&lt;y返回-1x&gt;y返回1
*/
@Override

View File

@ -3,7 +3,7 @@ package cn.hutool.core.lang.mutable;
import cn.hutool.core.util.NumberUtil;
/**
* 可变 <code>byte</code> 类型
* 可变 {@code byte} 类型
*
* @see Byte
* @since 3.0.1
@ -157,12 +157,12 @@ public class MutableByte extends Number implements Comparable<MutableByte>, Muta
* 相等需同时满足如下条件
* <ol>
* <li>非空</li>
* <li>类型为 {@link MutableByte}</li>
* <li>类型为 MutableByte</li>
* <li>值相等</li>
* </ol>
*
* @param obj 比对的对象
* @return 相同返回<code>true</code>否则 <code>false</code>
* @return 相同返回<code>true</code>否则 {@code false}
*/
@Override
public boolean equals(final Object obj) {
@ -181,7 +181,7 @@ public class MutableByte extends Number implements Comparable<MutableByte>, Muta
/**
* 比较
*
* @param other 其它 {@link MutableByte} 对象
* @param other 其它 MutableByte 对象
* @return x==y返回0x&lt;y返回-1x&gt;y返回1
*/
@Override

View File

@ -3,7 +3,7 @@ package cn.hutool.core.lang.mutable;
import java.io.Serializable;
/**
* 可变<code>Object</code>
* 可变{@code Object}
*
* @param <T> 可变的类型
* @since 3.0.1

View File

@ -129,7 +129,7 @@ public final class UrlBuilder implements Serializable {
*/
public static UrlBuilder of(String url, Charset charset) {
Assert.notBlank(url, "Url must be not blank!");
return of(URLUtil.url(url.trim()), charset);
return of(URLUtil.url(StrUtil.trim(url)), charset);
}
/**

View File

@ -312,7 +312,7 @@ public class UrlQuery {
private static String toStr(CharSequence str, Charset charset, boolean isEncode) {
String result = StrUtil.str(str);
if (isEncode) {
result = URLUtil.encodeAll(result, charset);
result = URLUtil.encodeFragment(result, charset);
}
return result;
}

View File

@ -1729,16 +1729,14 @@ public class CharSequenceUtil {
/**
* 切分字符串
*
* @param str 被切分的字符串
* @param text 被切分的字符串
* @param separator 分隔符字符
* @param limit 限制分片数
* @return 切分后的数组
*/
public static String[] splitToArray(CharSequence str, char separator, int limit) {
if (null == str) {
return new String[]{};
}
return StrSplitter.splitToArray(str.toString(), separator, limit, false, false);
public static String[] splitToArray(CharSequence text, char separator, int limit) {
Assert.notNull(text, "Text must be not null!");
return StrSplitter.splitToArray(text.toString(), separator, limit, false, false);
}
/**

View File

@ -1,5 +1,6 @@
package cn.hutool.core.text;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.PatternPool;
import cn.hutool.core.text.finder.CharFinder;
import cn.hutool.core.text.finder.CharMatcherFinder;
@ -146,7 +147,7 @@ public class StrSplitter {
/**
* 切分字符串忽略大小写
*
* @param str 被切分的字符串
* @param text 被切分的字符串
* @param separator 分隔符字符
* @param limit 限制分片数-1不限制
* @param isTrim 是否去除切分字符串后每个元素两边的空格
@ -154,8 +155,8 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.2.1
*/
public static List<String> splitIgnoreCase(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) {
return split(str, separator, limit, isTrim, ignoreEmpty, true);
public static List<String> splitIgnoreCase(CharSequence text, char separator, int limit, boolean isTrim, boolean ignoreEmpty) {
return split(text, separator, limit, isTrim, ignoreEmpty, true);
}
/**
@ -177,7 +178,7 @@ public class StrSplitter {
/**
* 切分字符串
*
* @param <R> 切分后的元素类型
* @param <R> 切分后的元素类型
* @param text 被切分的字符串
* @param separator 分隔符字符
* @param limit 限制分片数-1不限制
@ -204,7 +205,7 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.0.8
*/
public static String[] splitToArray(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) {
public static String[] splitToArray(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) {
return toArray(split(str, separator, limit, isTrim, ignoreEmpty));
}
@ -220,7 +221,7 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.0.8
*/
public static List<String> split(String str, String separator, boolean isTrim, boolean ignoreEmpty) {
public static List<String> split(CharSequence str, String separator, boolean isTrim, boolean ignoreEmpty) {
return split(str, separator, -1, isTrim, ignoreEmpty, false);
}
@ -233,7 +234,7 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.2.1
*/
public static List<String> splitTrim(String str, String separator, boolean ignoreEmpty) {
public static List<String> splitTrim(CharSequence str, String separator, boolean ignoreEmpty) {
return split(str, separator, true, ignoreEmpty);
}
@ -248,7 +249,7 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.0.8
*/
public static List<String> split(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) {
public static List<String> split(CharSequence str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) {
return split(str, separator, limit, isTrim, ignoreEmpty, false);
}
@ -262,7 +263,7 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.2.1
*/
public static List<String> splitTrim(String str, String separator, int limit, boolean ignoreEmpty) {
public static List<String> splitTrim(CharSequence str, String separator, int limit, boolean ignoreEmpty) {
return split(str, separator, limit, true, ignoreEmpty);
}
@ -277,7 +278,7 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.2.1
*/
public static List<String> splitIgnoreCase(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) {
public static List<String> splitIgnoreCase(CharSequence str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) {
return split(str, separator, limit, isTrim, ignoreEmpty, true);
}
@ -291,7 +292,7 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.2.1
*/
public static List<String> splitTrimIgnoreCase(String str, String separator, int limit, boolean ignoreEmpty) {
public static List<String> splitTrimIgnoreCase(CharSequence str, String separator, int limit, boolean ignoreEmpty) {
return split(str, separator, limit, true, ignoreEmpty, true);
}
@ -307,7 +308,7 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.2.1
*/
public static List<String> split(String text, String separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase) {
public static List<String> split(CharSequence text, String separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase) {
final SplitIter splitIter = new SplitIter(text, new StrFinder(separator, ignoreCase), limit, ignoreEmpty);
return splitIter.toList(isTrim);
}
@ -323,7 +324,7 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.0.8
*/
public static String[] splitToArray(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) {
public static String[] splitToArray(CharSequence str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) {
return toArray(split(str, separator, limit, isTrim, ignoreEmpty));
}
@ -338,7 +339,8 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.0.8
*/
public static List<String> split(String text, int limit) {
public static List<String> split(CharSequence text, int limit) {
if (StrUtil.isEmpty(text)) {
return new ArrayList<>(0);
}
@ -362,7 +364,7 @@ public class StrSplitter {
/**
* 通过正则切分字符串
*
* @param str 字符串
* @param text 字符串
* @param separatorRegex 分隔符正则
* @param limit 限制分片数
* @param isTrim 是否去除切分字符串后每个元素两边的空格
@ -370,9 +372,9 @@ public class StrSplitter {
* @return 切分后的集合
* @since 3.0.8
*/
public static List<String> splitByRegex(String str, String separatorRegex, int limit, boolean isTrim, boolean ignoreEmpty) {
public static List<String> splitByRegex(String text, String separatorRegex, int limit, boolean isTrim, boolean ignoreEmpty) {
final Pattern pattern = PatternPool.get(separatorRegex);
return split(str, pattern, limit, isTrim, ignoreEmpty);
return split(text, pattern, limit, isTrim, ignoreEmpty);
}
/**
@ -387,7 +389,8 @@ public class StrSplitter {
* @since 3.0.8
*/
public static List<String> split(String text, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty) {
if (StrUtil.isEmpty(text)) {
Assert.notNull(text, "Text must be not null!");
if (text.length() < 1) {
return new ArrayList<>(0);
}
final SplitIter splitIter = new SplitIter(text, new PatternFinder(separatorPattern), limit, ignoreEmpty);

View File

@ -53,7 +53,6 @@ public class SplitIter extends ComputeIter<String> implements Serializable {
@Override
protected String computeNext() {
Assert.notNull(this.text, "Text to find must be not null!");
// 达到数量上限或末尾结束
if (count >= limit || offset > text.length()) {
return null;

View File

@ -163,6 +163,16 @@ public class RandomUtil {
return 0 == randomInt(2);
}
/**
* 随机汉字'\u4E00'-'\u9FFF'
*
* @return 随机的汉字字符
* @since 5.7.15
*/
public static char randomChinese() {
return (char) randomInt('\u4E00', '\u9FFF');
}
/**
* 获得指定范围内的随机数
*

View File

@ -3,18 +3,23 @@ package cn.hutool.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Holder;
import cn.hutool.core.lang.PatternPool;
import cn.hutool.core.lang.RegexPool;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.lang.func.Func1;
import cn.hutool.core.lang.mutable.MutableObj;
import cn.hutool.core.map.MapUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -78,11 +83,27 @@ public class ReUtil {
return null;
}
// Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);
return get(pattern, content, groupIndex);
}
/**
* 获得匹配的字符串
*
* @param regex 匹配的正则
* @param content 被匹配的内容
* @param groupName 匹配正则的分组名称
* @return 匹配后得到的字符串未匹配返回null
*/
public static String get(String regex, CharSequence content, String groupName) {
if (null == content || null == regex) {
return null;
}
final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);
return get(pattern, content, groupName);
}
/**
* 获得匹配的字符串获得正则中分组0的内容
*
@ -120,11 +141,47 @@ public class ReUtil {
return null;
}
final Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
return matcher.group(groupIndex);
final MutableObj<String> result = new MutableObj<>();
get(pattern, content, matcher -> result.set(matcher.group(groupIndex)));
return result.get();
}
/**
* 获得匹配的字符串
*
* @param pattern 匹配的正则
* @param content 被匹配的内容
* @param groupName 匹配正则的分组名称
* @return 匹配后得到的字符串未匹配返回null
* @since 5.7.15
*/
public static String get(Pattern pattern, CharSequence content, String groupName) {
if (null == content || null == pattern || null == groupName) {
return null;
}
final MutableObj<String> result = new MutableObj<>();
get(pattern, content, matcher -> result.set(matcher.group(groupName)));
return result.get();
}
/**
* 在给定字符串中查找给定规则的字符如果找到则使用{@link Consumer}处理之<br>
* 如果内容中有多个匹配项则只处理找到的第一个结果
*
* @param pattern 匹配的正则
* @param content 被匹配的内容
* @param consumer 匹配到的内容处理器
* @since 5.7.15
*/
public static void get(Pattern pattern, CharSequence content, Consumer<Matcher> consumer) {
if (null == content || null == pattern || null == consumer) {
return;
}
final Matcher m = pattern.matcher(content);
if (m.find()) {
consumer.accept(m);
}
return null;
}
/**
@ -165,6 +222,33 @@ public class ReUtil {
return result;
}
/**
* 根据给定正则查找字符串中的匹配项返回所有匹配的分组名对应分组值<br>
* <pre>
* pattern: (?&lt;year&gt;\\d+)-(?&lt;month&gt;\\d+)-(?&lt;day&gt;\\d+)
* content: 2021-10-11
* result : year: 2021, month: 10, day: 11
* </pre>
*
* @param pattern 匹配的正则
* @param content 被匹配的内容
* @return 命名捕获组key为分组名value为对应值
* @since 5.7.15
*/
public static Map<String, String> getAllGroupNames(Pattern pattern, CharSequence content) {
if (null == content || null == pattern) {
return null;
}
final Matcher m = pattern.matcher(content);
final Map<String, String> result = MapUtil.newHashMap(m.groupCount());
if (m.find()) {
// 通过反射获取 namedGroups 方法
final Map<String, Integer> map = ReflectUtil.invoke(pattern, "namedGroups");
map.forEach((key, value) -> result.put(key, m.group(value)));
}
return result;
}
/**
* 从content中匹配出多个值并根据template生成新的字符串<br>
* 例如<br>
@ -213,7 +297,6 @@ public class ReUtil {
return null;
}
// Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);
return extractMulti(pattern, content, template);
}
@ -264,7 +347,6 @@ public class ReUtil {
return null;
}
// Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);
return extractMultiAndDelPre(pattern, contentHolder, template);
}
@ -281,7 +363,6 @@ public class ReUtil {
return StrUtil.str(content);
}
// Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);
return delFirst(pattern, content);
}
@ -300,8 +381,8 @@ public class ReUtil {
/**
* 替换匹配的第一个内容
*
* @param pattern 正则
* @param content 被匹配的内容
* @param pattern 正则
* @param content 被匹配的内容
* @param replacement 替换的内容
* @return 替换后剩余的内容
* @since 5.6.5
@ -342,7 +423,7 @@ public class ReUtil {
public static String delLast(Pattern pattern, CharSequence str) {
if (null != pattern && StrUtil.isNotEmpty(str)) {
final MatchResult matchResult = lastIndexOf(pattern, str);
if(null != matchResult){
if (null != matchResult) {
return StrUtil.subPre(str, matchResult.start()) + StrUtil.subSuf(str, matchResult.end());
}
}
@ -362,7 +443,6 @@ public class ReUtil {
return StrUtil.str(content);
}
// Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);
return delAll(pattern, content);
}
@ -394,9 +474,23 @@ public class ReUtil {
return StrUtil.str(content);
}
// Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);
Matcher matcher = pattern.matcher(content);
return delPre(pattern, content);
}
/**
* 删除正则匹配到的内容之前的字符 如果没有找到则返回原文
*
* @param pattern 定位正则模式
* @param content 被查找的内容
* @return 删除前缀后的新内容
*/
public static String delPre(Pattern pattern, CharSequence content) {
if (null == content || null == pattern) {
return StrUtil.str(content);
}
final Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
return StrUtil.sub(content, matcher.end(), content.length());
}
@ -455,7 +549,7 @@ public class ReUtil {
return collection;
}
return findAll(Pattern.compile(regex, Pattern.DOTALL), content, group, collection);
return findAll(PatternPool.get(regex, Pattern.DOTALL), content, group, collection);
}
/**
@ -509,16 +603,29 @@ public class ReUtil {
if (null == pattern || null == content) {
return null;
}
Assert.notNull(collection, "Collection must be not null !");
if (null == collection) {
throw new NullPointerException("Null collection param provided!");
findAll(pattern, content, (matcher) -> collection.add(matcher.group(group)));
return collection;
}
/**
* 取得内容中匹配的所有结果使用{@link Consumer}完成匹配结果处理
*
* @param pattern 编译后的正则模式
* @param content 被查找的内容
* @param consumer 匹配结果处理函数
* @since 5.7.15
*/
public static void findAll(Pattern pattern, CharSequence content, Consumer<Matcher> consumer) {
if (null == pattern || null == content) {
return;
}
final Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
collection.add(matcher.group(group));
consumer.accept(matcher);
}
return collection;
}
/**
@ -533,7 +640,6 @@ public class ReUtil {
return 0;
}
// Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL);
return count(pattern, content);
}
@ -594,12 +700,12 @@ public class ReUtil {
/**
* 找到指定正则匹配到字符串的开始位置
*
* @param regex 正则
* @param regex 正则
* @param content 字符串
* @return 位置{@code null}表示未找到
* @since 5.6.5
*/
public static MatchResult indexOf(String regex, CharSequence content){
public static MatchResult indexOf(String regex, CharSequence content) {
if (null == regex || null == content) {
return null;
}
@ -616,10 +722,10 @@ public class ReUtil {
* @return 位置{@code null}表示未找到
* @since 5.6.5
*/
public static MatchResult indexOf(Pattern pattern, CharSequence content){
if(null != pattern && null != content){
public static MatchResult indexOf(Pattern pattern, CharSequence content) {
if (null != pattern && null != content) {
final Matcher matcher = pattern.matcher(content);
if(matcher.find()){
if (matcher.find()) {
return matcher.toMatchResult();
}
}
@ -630,12 +736,12 @@ public class ReUtil {
/**
* 找到指定正则匹配到第一个字符串的位置
*
* @param regex 正则
* @param regex 正则
* @param content 字符串
* @return 位置{@code null}表示未找到
* @since 5.6.5
*/
public static MatchResult lastIndexOf(String regex, CharSequence content){
public static MatchResult lastIndexOf(String regex, CharSequence content) {
if (null == regex || null == content) {
return null;
}
@ -652,11 +758,11 @@ public class ReUtil {
* @return 位置{@code null}表示未找到
* @since 5.6.5
*/
public static MatchResult lastIndexOf(Pattern pattern, CharSequence content){
public static MatchResult lastIndexOf(Pattern pattern, CharSequence content) {
MatchResult result = null;
if(null != pattern && null != content){
if (null != pattern && null != content) {
final Matcher matcher = pattern.matcher(content);
while(matcher.find()){
while (matcher.find()) {
result = matcher.toMatchResult();
}
}

View File

@ -2,6 +2,7 @@ package cn.hutool.core.util;
import cn.hutool.core.compress.Deflate;
import cn.hutool.core.compress.Gzip;
import cn.hutool.core.compress.ZipCopyVisitor;
import cn.hutool.core.compress.ZipReader;
import cn.hutool.core.compress.ZipWriter;
import cn.hutool.core.exceptions.UtilException;
@ -9,6 +10,8 @@ import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileSystemUtil;
import cn.hutool.core.io.file.PathUtil;
import cn.hutool.core.io.resource.Resource;
import java.io.BufferedInputStream;
@ -20,6 +23,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.CopyOption;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -32,8 +40,8 @@ import java.util.zip.ZipOutputStream;
/**
* 压缩工具类
*
* @see cn.hutool.core.compress.ZipWriter
* @author Looly
* @see cn.hutool.core.compress.ZipWriter
*/
public class ZipUtil {
@ -75,6 +83,33 @@ public class ZipUtil {
}
}
/**
* 在zip文件中添加新文件或目录<br>
* 新文件添加在zip根目录文件夹包括其本身和内容<br>
* 如果待添加文件夹是系统根路径/或c:/则只复制文件夹下的内容
*
* @param zipPath zip文件的Path
* @param appendFilePath 待添加文件Path(可以是文件夹)
* @param options 拷贝选项可选是否覆盖等
* @since 5.7.15
*/
public static void append(Path zipPath, Path appendFilePath, CopyOption... options) throws IOException {
try (FileSystem zipFileSystem = FileSystemUtil.createZip(zipPath.toString())) {
if (Files.isDirectory(appendFilePath)) {
Path source = appendFilePath.getParent();
if (null == source) {
// 如果用户提供的是根路径则不复制目录直接复制目录下的内容
source = appendFilePath;
}
Files.walkFileTree(appendFilePath, new ZipCopyVisitor(source, zipFileSystem, options));
} else {
Files.copy(appendFilePath, zipFileSystem.getPath(PathUtil.getName(appendFilePath)), options);
}
} catch (FileAlreadyExistsException ignored) {
// 不覆盖情况下文件已存在, 跳过
}
}
/**
* 打包到当前目录使用默认编码UTF-8
*
@ -240,7 +275,7 @@ public class ZipUtil {
*/
@Deprecated
public static void zip(ZipOutputStream zipOutputStream, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException {
try(final ZipWriter zipWriter = new ZipWriter(zipOutputStream)){
try (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) {
zipWriter.add(withSrcDir, filter, srcFiles);
}
}
@ -339,7 +374,7 @@ public class ZipUtil {
throw new IllegalArgumentException("Paths length is not equals to ins length !");
}
try(final ZipWriter zipWriter = ZipWriter.of(zipFile, charset)){
try (final ZipWriter zipWriter = ZipWriter.of(zipFile, charset)) {
for (int i = 0; i < paths.length; i++) {
zipWriter.add(paths[i], ins[i]);
}
@ -364,7 +399,7 @@ public class ZipUtil {
throw new IllegalArgumentException("Paths length is not equals to ins length !");
}
try(final ZipWriter zipWriter = ZipWriter.of(out, DEFAULT_CHARSET)){
try (final ZipWriter zipWriter = ZipWriter.of(out, DEFAULT_CHARSET)) {
for (int i = 0; i < paths.length; i++) {
zipWriter.add(paths[i], ins[i]);
}
@ -388,7 +423,7 @@ public class ZipUtil {
throw new IllegalArgumentException("Paths length is not equals to ins length !");
}
try(final ZipWriter zipWriter = new ZipWriter(zipOutputStream)){
try (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) {
for (int i = 0; i < paths.length; i++) {
zipWriter.add(paths[i], ins[i]);
}
@ -528,7 +563,7 @@ public class ZipUtil {
StrUtil.format("Target path [{}] exist!", outFile.getAbsolutePath()));
}
try(final ZipReader reader = new ZipReader(zipFile)){
try (final ZipReader reader = new ZipReader(zipFile)) {
reader.readTo(outFile);
}
return outFile;
@ -571,7 +606,7 @@ public class ZipUtil {
* @since 5.5.2
*/
public static void read(ZipFile zipFile, Consumer<ZipEntry> consumer) {
try(final ZipReader reader = new ZipReader(zipFile)){
try (final ZipReader reader = new ZipReader(zipFile)) {
reader.read(consumer);
}
}
@ -605,7 +640,7 @@ public class ZipUtil {
* @since 4.5.8
*/
public static File unzip(ZipInputStream zipStream, File outFile) throws UtilException {
try(final ZipReader reader = new ZipReader(zipStream)){
try (final ZipReader reader = new ZipReader(zipStream)) {
reader.readTo(outFile);
}
return outFile;
@ -619,7 +654,7 @@ public class ZipUtil {
* @since 5.5.2
*/
public static void read(ZipInputStream zipStream, Consumer<ZipEntry> consumer) {
try(final ZipReader reader = new ZipReader(zipStream)){
try (final ZipReader reader = new ZipReader(zipStream)) {
reader.read(consumer);
}
}
@ -671,7 +706,7 @@ public class ZipUtil {
* @since 4.1.8
*/
public static byte[] unzipFileBytes(File zipFile, Charset charset, String name) {
try(final ZipReader reader = ZipReader.of(zipFile, charset)){
try (final ZipReader reader = ZipReader.of(zipFile, charset)) {
return IoUtil.readBytes(reader.get(name));
}
}

View File

@ -197,6 +197,19 @@ public class BeanUtilTest {
Assert.assertEquals("sub名字", map.get("sub_name"));
}
@Test
public void beanToMapWithValueEditTest() {
SubPerson person = new SubPerson();
person.setAge(14);
person.setOpenid("11213232");
person.setName("测试A11");
person.setSubName("sub名字");
Map<String, Object> map = BeanUtil.beanToMap(person, new LinkedHashMap<>(),
CopyOptions.create().setFieldValueEditor((key, value) -> key + "_" + value));
Assert.assertEquals("subName_sub名字", map.get("subName"));
}
@Test
public void beanToMapWithAliasTest() {
SubPersonWithAlias person = new SubPersonWithAlias();

View File

@ -0,0 +1,34 @@
package cn.hutool.core.collection;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ThreadUtil;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 集合索引环形获取工具类测试类
*
* @author ZhouChuGang
*/
public class RingIndexUtilTest {
private final List<String> strList = Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
/**
* 观察输出的打印为不重复的
*/
@Test
public void ringNextIntByObjTest() {
final AtomicInteger atomicInteger = new AtomicInteger();
// 开启并发测试每个线程获取到的元素都是唯一的
ThreadUtil.concurrencyTest(strList.size(), () -> {
final int index = RingIndexUtil.ringNextIntByObj(strList, atomicInteger);
final String s = strList.get(index);
Assert.notNull(s);
});
}
}

View File

@ -1,5 +1,8 @@
package cn.hutool.core.compress;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.FileResource;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ZipUtil;
import org.junit.Ignore;
import org.junit.Test;
@ -13,4 +16,12 @@ public class ZipWriterTest {
public void zipDirTest() {
ZipUtil.zip(new File("d:/test"));
}
@Test
@Ignore
public void addTest(){
final ZipWriter writer = ZipWriter.of(FileUtil.file("d:/test/test.zip"), CharsetUtil.CHARSET_UTF_8);
writer.add(new FileResource("d:/test/qr_c.png"));
writer.close();
}
}

View File

@ -989,4 +989,10 @@ public class DateUtilTest {
Assert.assertNotNull(parse);
Assert.assertEquals("2021-01-01 00:00:00", parse.toString());
}
@Test
public void parseByDateTimeFormatterTest(){
final DateTime parse = DateUtil.parse("2021-12-01", DatePattern.NORM_DATE_FORMATTER);
Assert.assertEquals("2021-12-01 00:00:00", parse.toString());
}
}

View File

@ -0,0 +1,31 @@
package cn.hutool.core.io.file;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.CharsetUtil;
import org.junit.Ignore;
import org.junit.Test;
import java.nio.file.FileSystem;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
public class FileSystemUtilTest {
@Test
@Ignore
public void listTest(){
final FileSystem fileSystem = FileSystemUtil.createZip("d:/test/test.zip",
CharsetUtil.CHARSET_GBK);
final Path root = FileSystemUtil.getRoot(fileSystem);
PathUtil.walkFiles(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
Console.log(path);
return FileVisitResult.CONTINUE;
}
});
}
}

View File

@ -192,9 +192,9 @@ public class UrlBuilderTest {
"&amp;sn=1044c0d19723f74f04f4c1da34eefa35" +
"&amp;chksm=6cbda3a25bca2ab4516410db6ce6e125badaac2f8c5548ea6e18eab6dc3c5422cb8cbe1095f7";
final UrlBuilder builder = UrlBuilder.ofHttp(urlStr, CharsetUtil.CHARSET_UTF_8);
// 原URL中的&amp;替换为&value中的=被编码为%3D
// 原URL中的&amp;替换为&
Assert.assertEquals("https://mp.weixin.qq.com/s?" +
"__biz=MzI5NjkyNTIxMg%3D%3D" +
"__biz=MzI5NjkyNTIxMg==" +
"&mid=100000465&idx=1" +
"&sn=1044c0d19723f74f04f4c1da34eefa35" +
"&chksm=6cbda3a25bca2ab4516410db6ce6e125badaac2f8c5548ea6e18eab6dc3c5422cb8cbe1095f7",
@ -240,7 +240,7 @@ public class UrlBuilderTest {
public void testEncodeInQuery() {
String webUrl = "http://exmple.com/patha/pathb?a=123&b=4?6&c=789"; // b=4?6 参数中有未编码的
final UrlBuilder urlBuilder = UrlBuilder.of(webUrl, StandardCharsets.UTF_8);
Assert.assertEquals("a=123&b=4%3F6&c=789", urlBuilder.getQueryStr());
Assert.assertEquals("a=123&b=4?6&c=789", urlBuilder.getQueryStr());
}
@Test
@ -271,4 +271,14 @@ public class UrlBuilderTest {
urlBuilder = UrlBuilder.ofHttp(urlBuilder.toString());
Assert.assertEquals(urlBuilder.toString(), urlBuilder.toString());
}
@Test
public void slashEncodeTest(){
// https://github.com/dromara/hutool/issues/1904
// 在query中"/"是不可转义字符
// https://www.rfc-editor.org/rfc/rfc3986.html#section-3.4
String url = "https://invoice.maycur.com/2b27a802-8423-4d41-86f5-63a6b259f61e.xlsx?download/2b27a802-8423-4d41-86f5-63a6b259f61e.xlsx&e=1630491088";
final UrlBuilder urlBuilder = UrlBuilder.ofHttp(url);
Assert.assertEquals(url, urlBuilder.toString());
}
}

View File

@ -34,7 +34,7 @@ public class StrJoinerTest {
public void joinMultiArrayTest(){
final StrJoiner append = StrJoiner.of(",");
append.append(new Object[]{ListUtil.of("1", "2"),
CollUtil.newHashSet("3", "4")
CollUtil.newLinkedHashSet("3", "4")
});
Assert.assertEquals("1,2,3,4", append.toString());
}

View File

@ -53,4 +53,10 @@ public class RandomUtilTest {
final byte[] c = RandomUtil.randomBytes(10);
Assert.assertNotNull(c);
}
@Test
public void randomChineseTest(){
char c = RandomUtil.randomChinese();
Assert.assertTrue(c > 0);
}
}

View File

@ -8,6 +8,7 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class ReUtilTest {
@ -163,4 +164,26 @@ public class ReUtilTest {
"(.+?)省(.+?)市(.+?)区", "广东省深圳市南山区");
Console.log(match);
}
@Test
public void getByGroupNameTest() {
String content = "2021-10-11";
String regex = "(?<year>\\d+)-(?<month>\\d+)-(?<day>\\d+)";
String year = ReUtil.get(regex, content, "year");
Assert.assertEquals("2021", year);
String month = ReUtil.get(regex, content, "month");
Assert.assertEquals("10", month);
String day = ReUtil.get(regex, content, "day");
Assert.assertEquals("11", day);
}
@Test
public void getAllGroupNamesTest() {
String content = "2021-10-11";
String regex = "(?<year>\\d+)-(?<month>\\d+)-(?<day>\\d+)";
Map<String, String> map = ReUtil.getAllGroupNames(PatternPool.get(regex, Pattern.DOTALL), content);
Assert.assertEquals(map.get("year"), "2021");
Assert.assertEquals(map.get("month"), "10");
Assert.assertEquals(map.get("day"), "11");
}
}

View File

@ -81,6 +81,16 @@ public class StrUtilTest {
Assert.assertEquals("", split.get(2));
}
@Test(expected = IllegalArgumentException.class)
public void splitNullTest() {
StrUtil.split(null, '.');
}
@Test(expected = IllegalArgumentException.class)
public void splitToArrayNullTest() {
StrUtil.splitToArray(null, '.');
}
@Test
public void splitToLongTest() {
String str = "1,2,3,4, 5";

View File

@ -1,5 +1,6 @@
package cn.hutool.core.util;
import cn.hutool.core.compress.ZipReader;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.lang.Console;
@ -12,6 +13,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
* {@link ZipUtil}单元测试
@ -20,6 +23,54 @@ import java.nio.charset.Charset;
*/
public class ZipUtilTest {
@Test
public void appendTest() throws IOException {
File appendFile = FileUtil.file("test-zip/addFile.txt");
File zipFile = FileUtil.file("test-zip/test.zip");
// 用于测试完成后将被测试文件恢复
File tempZipFile = FileUtil.createTempFile(FileUtil.file("test-zip"));
tempZipFile.deleteOnExit();
FileUtil.copy(zipFile, tempZipFile, true);
// test file add
List<String> beforeNames = zipEntryNames(tempZipFile);
ZipUtil.append(tempZipFile.toPath(), appendFile.toPath());
List<String> afterNames = zipEntryNames(tempZipFile);
// 确认增加了文件
Assert.assertEquals(beforeNames.size() + 1, afterNames.size());
Assert.assertTrue(afterNames.containsAll(beforeNames));
Assert.assertTrue(afterNames.contains(appendFile.getName()));
// test dir add
beforeNames = zipEntryNames(tempZipFile);
File addDirFile = FileUtil.file("test-zip/test-add");
ZipUtil.append(tempZipFile.toPath(), addDirFile.toPath());
afterNames = zipEntryNames(tempZipFile);
// 确认增加了文件和目录增加目录和目录下一个文件故此处+2
Assert.assertEquals(beforeNames.size() + 2, afterNames.size());
Assert.assertTrue(afterNames.containsAll(beforeNames));
Assert.assertTrue(afterNames.contains(appendFile.getName()));
// rollback
Assert.assertTrue(String.format("delete temp file %s failed", tempZipFile.getCanonicalPath()), tempZipFile.delete());
}
/**
* 获取zip文件中所有一级文件/文件夹的name
*
* @param zipFile 待测试的zip文件
* @return zip文件中一级目录下的所有文件/文件夹名
*/
private List<String> zipEntryNames(File zipFile) {
List<String> fileNames = new ArrayList<>();
ZipReader reader = ZipReader.of(zipFile, CharsetUtil.CHARSET_UTF_8);
reader.read(zipEntry -> fileNames.add(zipEntry.getName()));
reader.close();
return fileNames;
}
@Test
@Ignore
@ -109,4 +160,24 @@ public class ZipUtilTest {
}
}
@Test
@Ignore
public void zipStreamTest2(){
// https://github.com/dromara/hutool/issues/944
String file1 = "d:/test/a.txt";
String file2 = "d:/test/a.txt";
String file3 = "d:/test/asn1.key";
String zip = "d:/test/test2.zip";
try (OutputStream out = new FileOutputStream(zip)){
//实际应用中, out HttpServletResponse.getOutputStream
ZipUtil.zip(out, Charset.defaultCharset(), false, null,
new File(file1),
new File(file2),
new File(file3)
);
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
}

View File

@ -0,0 +1,2 @@
this file will be used to add into the test.zip
before the add action, the test.zip won't have this file.

View File

@ -0,0 +1 @@
1

Binary file not shown.

View File

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

View File

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

View File

@ -180,7 +180,7 @@ public class KeyUtil {
*/
public static SecretKey generateDESKey(String algorithm, byte[] key) {
if (StrUtil.isBlank(algorithm) || false == algorithm.startsWith("DES")) {
throw new CryptoException("Algorithm [{}] is not a DES algorithm!");
throw new CryptoException("Algorithm [{}] is not a DES algorithm!", algorithm);
}
SecretKey secretKey;
@ -212,7 +212,7 @@ public class KeyUtil {
*/
public static SecretKey generatePBEKey(String algorithm, char[] key) {
if (StrUtil.isBlank(algorithm) || false == algorithm.startsWith("PBE")) {
throw new CryptoException("Algorithm [{}] is not a PBE algorithm!");
throw new CryptoException("Algorithm [{}] is not a PBE algorithm!", algorithm);
}
if (null == key) {

View File

@ -300,9 +300,11 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
throw new CryptoException(e);
} finally {
lock.unlock();
// issue#I4EMST@Gitee
// CipherOutputStream必须关闭才能完全写出
IoUtil.close(cipherOutputStream);
if (isClose) {
IoUtil.close(data);
IoUtil.close(cipherOutputStream);
}
}
}
@ -351,9 +353,11 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
throw new CryptoException(e);
} finally {
lock.unlock();
// issue#I4EMST@Gitee
// CipherOutputStream必须关闭才能完全写出
IoUtil.close(cipherInputStream);
if (isClose) {
IoUtil.close(data);
IoUtil.close(cipherInputStream);
}
}
}

View File

@ -81,4 +81,6 @@ public class SmTest {
String digest = hMac.digestHex(content);
Assert.assertEquals("493e3f9a1896b43075fbe54658076727960d69632ac6b6ed932195857a6840c6", digest);
}
}

View File

@ -0,0 +1,51 @@
package cn.hutool.crypto.test.symmetric;
import cn.hutool.crypto.symmetric.SM4;
import org.junit.Ignore;
import org.junit.Test;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* https://gitee.com/dromara/hutool/issues/I4EMST
*/
public class Sm4StreamTest {
private static final SM4 sm4 = new SM4();
private static final boolean IS_CLOSE = false;
@Test
@Ignore
public void sm4Test(){
String source = "d:/test/sm4_1.txt";
String target = "d:/test/sm4_2.data";
String target2 = "d:/test/sm4_3.txt";
encrypt(source, target);
decrypt(target, target2);
}
public static void encrypt(String source, String target) {
try (InputStream input = new FileInputStream(source);
OutputStream out = new FileOutputStream(target)) {
sm4.encrypt(input, out, IS_CLOSE);
System.out.println("============encrypt end");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void decrypt(String source, String target) {
try (InputStream input = new FileInputStream(source);
OutputStream out = new FileOutputStream(target)) {
sm4.decrypt(input, out, IS_CLOSE);
System.out.println("============decrypt end");
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.14</version>
<version>5.7.15</version>
</parent>
<artifactId>hutool-db</artifactId>
@ -143,13 +143,13 @@
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
<version>8.0.27</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.23.jre7</version>
<version>42.3.0</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@ -1,10 +1,5 @@
package cn.hutool.db;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import cn.hutool.core.lang.func.VoidFunc1;
import cn.hutool.db.dialect.Dialect;
import cn.hutool.db.dialect.DialectFactory;
@ -13,10 +8,14 @@ import cn.hutool.db.sql.Wrapper;
import cn.hutool.db.transaction.TransactionLevel;
import cn.hutool.log.StaticLog;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 数据库操作类<br>
* 通过给定的数据源执行给定SQL或者给定数据源和方言执行相应的CRUD操作<br>
*
*
* @author Looly
* @since 4.1.2
*/
@ -26,7 +25,7 @@ public class Db extends AbstractDb {
/**
* 创建Db<br>
* 使用默认数据源自动探测数据库连接池
*
*
* @return Db
*/
public static Db use() {
@ -36,7 +35,7 @@ public class Db extends AbstractDb {
/**
* 创建Db<br>
* 使用默认数据源自动探测数据库连接池
*
*
* @param group 数据源分组
* @return Db
*/
@ -47,7 +46,7 @@ public class Db extends AbstractDb {
/**
* 创建Db<br>
* 会根据数据源连接的元信息识别目标数据库类型进而使用合适的数据源
*
*
* @param ds 数据源
* @return Db
*/
@ -57,7 +56,7 @@ public class Db extends AbstractDb {
/**
* 创建Db
*
*
* @param ds 数据源
* @param dialect 方言
* @return Db
@ -68,7 +67,7 @@ public class Db extends AbstractDb {
/**
* 创建Db
*
*
* @param ds 数据源
* @param driverClassName 数据库连接驱动类名
* @return Db
@ -80,7 +79,7 @@ public class Db extends AbstractDb {
// ---------------------------------------------------------------------------- Constructor start
/**
* 构造从DataSource中识别方言
*
*
* @param ds 数据源
*/
public Db(DataSource ds) {
@ -89,7 +88,7 @@ public class Db extends AbstractDb {
/**
* 构造
*
*
* @param ds 数据源
* @param driverClassName 数据库连接驱动类名用于识别方言
*/
@ -99,7 +98,7 @@ public class Db extends AbstractDb {
/**
* 构造
*
*
* @param ds 数据源
* @param dialect 方言
*/
@ -118,7 +117,7 @@ public class Db extends AbstractDb {
public Db setWrapper(Wrapper wrapper) {
return (Db) super.setWrapper(wrapper);
}
@Override
public Db disableWrapper() {
return (Db)super.disableWrapper();
@ -147,7 +146,7 @@ public class Db extends AbstractDb {
/**
* 执行事务使用默认的事务级别<br>
* 在同一事务中所有对数据库操作都是原子的同时提交或者同时回滚
*
*
* @param func 事务函数所有操作应在同一函数下执行确保在同一事务中
* @return this
* @throws SQLException SQL异常
@ -159,7 +158,7 @@ public class Db extends AbstractDb {
/**
* 执行事务<br>
* 在同一事务中所有对数据库操作都是原子的同时提交或者同时回滚
*
*
* @param transactionLevel 事务级别枚举null表示使用JDBC默认事务
* @param func 事务函数所有操作应在同一函数下执行确保在同一事务中
* @return this
@ -208,7 +207,7 @@ public class Db extends AbstractDb {
// ---------------------------------------------------------------------------- Private method start
/**
* 静默回滚事务
*
*
* @param conn Connection
*/
private void quietRollback(Connection conn) {
@ -223,12 +222,12 @@ public class Db extends AbstractDb {
/**
* 静默设置自动提交
*
*
* @param conn Connection
* @param autoCommit 是否自动提交
*/
private void quietSetAutoCommit(Connection conn, Boolean autoCommit) {
if (null != autoCommit) {
if (null != conn && null != autoCommit) {
try {
conn.setAutoCommit(autoCommit);
} catch (Exception e) {
@ -237,4 +236,4 @@ public class Db extends AbstractDb {
}
}
// ---------------------------------------------------------------------------- Private method end
}
}

View File

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

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.14</version>
<version>5.7.15</version>
</parent>
<artifactId>hutool-extra</artifactId>
@ -30,7 +30,7 @@
<net.version>3.7.2</net.version>
<emoji-java.version>5.1.1</emoji-java.version>
<servlet-api.version>4.0.1</servlet-api.version>
<spring-boot.version>2.5.5</spring-boot.version>
<spring-boot.version>2.5.6</spring-boot.version>
<cglib.version>3.3.0</cglib.version>
</properties>
@ -241,7 +241,7 @@
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId>
<version>8.10.0</version>
<version>8.10.1</version>
<optional>true</optional>
</dependency>
<dependency>
@ -326,7 +326,7 @@
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>pinyin</artifactId>
<version>0.2.1</version>
<version>0.2.2</version>
<optional>true</optional>
</dependency>
<dependency>
@ -418,7 +418,7 @@
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.4.12.Final</version>
<version>2.4.13.Final</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@ -432,7 +432,7 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.10</version>
<version>5.3.12</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>

View File

@ -470,9 +470,9 @@ public class Ftp extends AbstractFtp {
* 上传文件到指定目录可选
*
* <pre>
* 1. path为null或""上传到当前路径
* 2. path为相对路径则相对于当前路径的子路径
* 3. path为绝对路径则上传到此路径
* 1. destPath为null或""上传到当前路径
* 2. destPath为相对路径则相对于当前路径的子路径
* 3. destPath为绝对路径则上传到此路径
* </pre>
*
* @param destPath 服务端路径可以为{@code null} 或者相对路径或绝对路径
@ -495,14 +495,14 @@ public class Ftp extends AbstractFtp {
* </pre>
*
* @param file 文件
* @param path 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param destPath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param fileName 自定义在服务端保存的文件名
* @return 是否上传成功
* @throws IORuntimeException IO异常
*/
public boolean upload(String path, String fileName, File file) throws IORuntimeException {
public boolean upload(String destPath, String fileName, File file) throws IORuntimeException {
try (InputStream in = FileUtil.getInputStream(file)) {
return upload(path, fileName, in);
return upload(destPath, fileName, in);
} catch (IOException e) {
throw new IORuntimeException(e);
}
@ -517,13 +517,13 @@ public class Ftp extends AbstractFtp {
* 3. path为绝对路径则上传到此路径
* </pre>
*
* @param path 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param destPath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param fileName 文件名
* @param fileStream 文件流
* @return 是否上传成功
* @throws IORuntimeException IO异常
*/
public boolean upload(String path, String fileName, InputStream fileStream) throws IORuntimeException {
public boolean upload(String destPath, String fileName, InputStream fileStream) throws IORuntimeException {
try {
client.setFileType(FTPClient.BINARY_FILE_TYPE);
} catch (IOException e) {
@ -535,10 +535,10 @@ public class Ftp extends AbstractFtp {
pwd = pwd();
}
if (StrUtil.isNotBlank(path)) {
mkDirs(path);
if (false == isDir(path)) {
throw new FtpException("Change dir to [{}] error, maybe dir not exist!", path);
if (StrUtil.isNotBlank(destPath)) {
mkDirs(destPath);
if (false == isDir(destPath)) {
throw new FtpException("Change dir to [{}] error, maybe dir not exist!", destPath);
}
}

View File

@ -7,8 +7,6 @@ import cn.hutool.extra.ssh.Sftp;
import org.junit.Ignore;
import org.junit.Test;
import java.util.List;
public class FtpTest {
@Test
@ -25,12 +23,9 @@ public class FtpTest {
@Test
@Ignore
public void uploadTest() {
Ftp ftp = new Ftp("looly.centos");
Ftp ftp = new Ftp("localhost");
List<String> ls = ftp.ls("/file");
Console.log(ls);
boolean upload = ftp.upload("/file/aaa", FileUtil.file("E:/qrcodeWithLogo.jpg"));
boolean upload = ftp.upload("/temp", FileUtil.file("d:/test/test.zip"));
Console.log(upload);
IoUtil.close(ftp);

View File

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

View File

@ -45,6 +45,7 @@ public enum ContentType {
/**
* 构造
*
* @param value ContentType值
*/
ContentType(String value) {
@ -141,4 +142,16 @@ public enum ContentType {
public static String build(String contentType, Charset charset) {
return StrUtil.format("{};charset={}", contentType, charset.name());
}
/**
* 输出Content-Type字符串附带编码信息
*
* @param contentType Content-Type 枚举类型
* @param charset 编码
* @return Content-Type字符串
* @since 5.7.15
*/
public static String build(ContentType contentType, Charset charset) {
return build(contentType.getValue(), charset);
}
}

View File

@ -21,16 +21,16 @@ public class Engine extends UserAgentInfo {
/**
* 支持的引擎类型
*/
public static final List<Engine> engines = CollUtil.newArrayList(//
new Engine("Trident", "trident"), //
new Engine("Webkit", "webkit"), //
new Engine("Chrome", "chrome"), //
new Engine("Opera", "opera"), //
new Engine("Presto", "presto"), //
new Engine("Gecko", "gecko"), //
new Engine("KHTML", "khtml"), //
new Engine("Konqeror", "konqueror"), //
new Engine("MIDP", "MIDP")//
public static final List<Engine> engines = CollUtil.newArrayList(
new Engine("Trident", "trident"),
new Engine("Webkit", "webkit"),
new Engine("Chrome", "chrome"),
new Engine("Opera", "opera"),
new Engine("Presto", "presto"),
new Engine("Gecko", "gecko"),
new Engine("KHTML", "khtml"),
new Engine("Konqueror", "konqueror"),
new Engine("MIDP", "MIDP")
);
private final Pattern versionPattern;

View File

@ -0,0 +1,19 @@
package cn.hutool.http;
import cn.hutool.core.util.CharsetUtil;
import org.junit.Assert;
import org.junit.Test;
/**
* ContentType 单元测试
*
*
*/
public class ContentTypeTest {
@Test
public void testBuild() {
String result = ContentType.build(ContentType.JSON, CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("application/json;charset=UTF-8", result);
}
}

View File

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

View File

@ -3,6 +3,8 @@ package cn.hutool.json;
import cn.hutool.core.bean.BeanPath;
import cn.hutool.core.collection.ArrayIter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.text.StrJoiner;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
@ -12,6 +14,7 @@ import cn.hutool.json.serialize.GlobalSerializeMapping;
import cn.hutool.json.serialize.JSONSerializer;
import cn.hutool.json.serialize.JSONWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
@ -529,11 +532,48 @@ public class JSONArray implements JSON, JSONGetter<Integer>, List<Object>, Rando
return this.toJSONString(0);
}
/**
* 返回JSON字符串<br>
* 支持过滤器即选择哪些字段或值不写出
*
* @param indentFactor 每层缩进空格数
* @param filter 键值对过滤器
* @return JSON字符串
* @since 5.7.15
*/
public String toJSONString(int indentFactor, Filter<Pair<Integer, Object>> filter){
final StringWriter sw = new StringWriter();
synchronized (sw.getBuffer()) {
return this.write(sw, indentFactor, 0, filter).toString();
}
}
@Override
public Writer write(Writer writer, int indentFactor, int indent) throws JSONException {
return write(writer, indentFactor, indent, null);
}
/**
* 将JSON内容写入Writer<br>
* 支持过滤器即选择哪些字段或值不写出
*
* @param writer writer
* @param indentFactor 缩进因子定义每一级别增加的缩进量
* @param indent 本级别缩进量
* @param filter 过滤器
* @return Writer
* @throws JSONException JSON相关异常
* @since 5.7.15
*/
public Writer write(Writer writer, int indentFactor, int indent, Filter<Pair<Integer, Object>> filter) throws JSONException {
final JSONWriter jsonWriter = JSONWriter.of(writer, indentFactor, indent, config)
.beginArray();
this.forEach(jsonWriter::writeValue);
CollUtil.forEach(this, (value, index)->{
if (null == filter || filter.accept(new Pair<>(index, value))) {
jsonWriter.writeValue(value);
}
});
jsonWriter.end();
// 此处不关闭Writer考虑writer后续还需要填内容
return writer;

View File

@ -6,6 +6,8 @@ import cn.hutool.core.bean.copier.BeanCopier;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.map.CaseInsensitiveLinkedMap;
import cn.hutool.core.map.CaseInsensitiveMap;
import cn.hutool.core.map.MapUtil;
@ -18,6 +20,7 @@ import cn.hutool.json.serialize.JSONObjectSerializer;
import cn.hutool.json.serialize.JSONSerializer;
import cn.hutool.json.serialize.JSONWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
@ -552,17 +555,54 @@ public class JSONObject implements JSON, JSONGetter<String>, Map<String, Object>
return this.toJSONString(0);
}
/**
* 返回JSON字符串<br>
* 支持过滤器即选择哪些字段或值不写出
*
* @param indentFactor 每层缩进空格数
* @param filter 键值对过滤器
* @return JSON字符串
* @since 5.7.15
*/
public String toJSONString(int indentFactor, Filter<Pair<String, Object>> filter){
final StringWriter sw = new StringWriter();
synchronized (sw.getBuffer()) {
return this.write(sw, indentFactor, 0, filter).toString();
}
}
@Override
public Writer write(Writer writer, int indentFactor, int indent) throws JSONException {
return write(writer, indentFactor, indent, null);
}
/**
* 将JSON内容写入Writer<br>
* 支持过滤器即选择哪些字段或值不写出
*
* @param writer writer
* @param indentFactor 缩进因子定义每一级别增加的缩进量
* @param indent 本级别缩进量
* @param filter 过滤器
* @return Writer
* @throws JSONException JSON相关异常
* @since 5.7.15
*/
public Writer write(Writer writer, int indentFactor, int indent, Filter<Pair<String, Object>> filter) throws JSONException {
final JSONWriter jsonWriter = JSONWriter.of(writer, indentFactor, indent, config)
.beginObj();
this.forEach(jsonWriter::writeField);
this.forEach((key, value) -> {
if (null == filter || filter.accept(new Pair<>(key, value))) {
jsonWriter.writeField(key, value);
}
});
jsonWriter.end();
// 此处不关闭Writer考虑writer后续还需要填内容
return writer;
}
// ------------------------------------------------------------------------------------------------- Private method start
/**
* Bean对象转Map
*

View File

@ -62,13 +62,13 @@ public class JSONWriter extends Writer {
private boolean arrayMode;
/**
* 创建{@link JSONWriter}
* 创建JSONWriter
*
* @param writer {@link Writer}
* @param indentFactor 缩进因子定义每一级别增加的缩进量
* @param indent 本级别缩进量
* @param config JSON选项
* @return {@link JSONWriter}
* @return JSONWriter
*/
public static JSONWriter of(Writer writer, int indentFactor, int indent, JSONConfig config) {
return new JSONWriter(writer, indentFactor, indent, config);

View File

@ -239,4 +239,36 @@ public class JSONArrayTest {
private Integer id;
private String name;
}
@Test
public void filterIncludeTest(){
JSONArray json1 = JSONUtil.createArray()
.set("value1")
.set("value2")
.set("value3")
.set(true);
final String s = json1.toJSONString(0, (pair) -> pair.getValue().equals("value2"));
Assert.assertEquals("[\"value2\"]", s);
}
@Test
public void filterExcludeTest(){
JSONArray json1 = JSONUtil.createArray()
.set("value1")
.set("value2")
.set("value3")
.set(true);
final String s = json1.toJSONString(0, (pair) -> false == pair.getValue().equals("value2"));
Assert.assertEquals("[\"value1\",\"value3\",true]", s);
}
@Test
public void putNullTest(){
final JSONArray array = JSONUtil.createArray(JSONConfig.create().setIgnoreNullValue(false));
array.set(null);
Assert.assertEquals("[null]", array.toString());
}
}

View File

@ -609,4 +609,28 @@ public class JSONObjectTest {
class BigDecimalBean{
private BigDecimal orderId;
}
@Test
public void filterIncludeTest(){
JSONObject json1 = JSONUtil.createObj(JSONConfig.create().setOrder(true))
.set("a", "value1")
.set("b", "value2")
.set("c", "value3")
.set("d", true);
final String s = json1.toJSONString(0, (pair) -> pair.getKey().equals("b"));
Assert.assertEquals("{\"b\":\"value2\"}", s);
}
@Test
public void filterExcludeTest(){
JSONObject json1 = JSONUtil.createObj(JSONConfig.create().setOrder(true))
.set("a", "value1")
.set("b", "value2")
.set("c", "value3")
.set("d", true);
final String s = json1.toJSONString(0, (pair) -> false == pair.getKey().equals("b"));
Assert.assertEquals("{\"a\":\"value1\",\"c\":\"value3\",\"d\":true}", s);
}
}

View File

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

View File

@ -69,4 +69,14 @@ public class JWTValidatorTest {
boolean validate = JWT.of(token).setKey(key).validate(0);
Assert.assertFalse(validate);
}
@Test(expected = ValidateException.class)
public void validateDateTest(){
final JWT jwt = JWT.create()
.setPayload("id", 123)
.setPayload("username", "hutool")
.setExpiresAt(DateUtil.parse("2021-10-13 09:59:00"));
JWTValidator.of(jwt).validateDate(DateUtil.date());
}
}

View File

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

View File

@ -9,7 +9,7 @@
<parent>
<groupId>cn.hutool</groupId>
<artifactId>hutool-parent</artifactId>
<version>5.7.14</version>
<version>5.7.15</version>
</parent>
<artifactId>hutool-poi</artifactId>
@ -45,7 +45,7 @@
<dependency>
<groupId>org.ofdrw</groupId>
<artifactId>ofdrw-full</artifactId>
<version>1.15.5</version>
<version>1.16.0</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>

View File

@ -281,11 +281,13 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
short columnSize;
for (int y = startRowIndex; y <= endRowIndex; y++) {
row = this.sheet.getRow(y);
columnSize = row.getLastCellNum();
Cell cell;
for (short x = 0; x < columnSize; x++) {
cell = row.getCell(x);
cellHandler.handle(cell, CellUtil.getCellValue(cell));
if(null != row){
columnSize = row.getLastCellNum();
Cell cell;
for (short x = 0; x < columnSize; x++) {
cell = row.getCell(x);
cellHandler.handle(cell, CellUtil.getCellValue(cell));
}
}
}
}

View File

@ -7,7 +7,6 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Console;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
@ -765,7 +764,6 @@ public class ExcelWriter extends ExcelBase<ExcelWriter> {
if (null != content) {
final Cell cell = getOrCreateCell(firstColumn, firstRow);
CellUtil.setCellValue(cell, content, cellStyle);
Console.log("{} {} {}", firstColumn, firstRow, cell.getStringCellValue());
}
return this;
}

View File

@ -0,0 +1,46 @@
package cn.hutool.poi.excel;
import cn.hutool.poi.excel.cell.CellEditor;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.poi.ss.usermodel.Cell;
import org.junit.Assert;
import java.io.Serializable;
import java.util.List;
public class CellEditorTest {
@org.junit.Test
public void readTest(){
ExcelReader excelReader= ExcelUtil.getReader("cell_editor_test.xlsx");
excelReader.setCellEditor(new ExcelHandler());
List<Test> excelReaderObjects=excelReader.readAll(Test.class);
Assert.assertEquals("0", excelReaderObjects.get(0).getTest1());
Assert.assertEquals("b", excelReaderObjects.get(0).getTest2());
Assert.assertEquals("0", excelReaderObjects.get(1).getTest1());
Assert.assertEquals("b1", excelReaderObjects.get(1).getTest2());
Assert.assertEquals("0", excelReaderObjects.get(2).getTest1());
Assert.assertEquals("c2", excelReaderObjects.get(2).getTest2());
}
@AllArgsConstructor
@Data
public static class Test implements Serializable {
private static final long serialVersionUID = 1L;
private String test1;
private String test2;
}
public static class ExcelHandler implements CellEditor {
@ Override
public Object edit(Cell cell, Object o) {
if (cell.getColumnIndex()==0 && cell.getRowIndex() != 0){
o="0";
}
return o;
}
}
}

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