From 57be48c6a36d23bc9b298872c1a0fe09860e24b2 Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 2 Dec 2020 17:35:34 +0800 Subject: [PATCH 01/47] prepare 5.5.3 --- README-EN.md | 8 ++++---- README.md | 8 ++++---- bin/version.txt | 2 +- docs/js/version.js | 2 +- hutool-all/pom.xml | 2 +- hutool-aop/pom.xml | 2 +- hutool-bloomFilter/pom.xml | 2 +- hutool-bom/pom.xml | 2 +- hutool-cache/pom.xml | 2 +- hutool-captcha/pom.xml | 2 +- hutool-core/pom.xml | 2 +- hutool-cron/pom.xml | 2 +- hutool-crypto/pom.xml | 2 +- hutool-db/pom.xml | 2 +- hutool-dfa/pom.xml | 2 +- hutool-extra/pom.xml | 2 +- hutool-http/pom.xml | 2 +- hutool-json/pom.xml | 2 +- hutool-log/pom.xml | 2 +- hutool-poi/pom.xml | 2 +- hutool-script/pom.xml | 2 +- hutool-setting/pom.xml | 2 +- hutool-socket/pom.xml | 2 +- hutool-system/pom.xml | 2 +- pom.xml | 2 +- 25 files changed, 31 insertions(+), 31 deletions(-) diff --git a/README-EN.md b/README-EN.md index d985be568..472d6b799 100644 --- a/README-EN.md +++ b/README-EN.md @@ -125,19 +125,19 @@ Each module can be introduced individually, or all modules can be introduced by cn.hutool hutool-all - 5.5.2 + 5.5.3 ``` ### Gradle ``` -compile 'cn.hutool:hutool-all:5.5.2' +compile 'cn.hutool:hutool-all:5.5.3' ``` ## Download -- [Maven1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.5.2/) -- [Maven2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.5.2/) +- [Maven1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.5.3/) +- [Maven2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.5.3/) > 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. diff --git a/README.md b/README.md index 7a4cf2e6e..76d6ef33f 100644 --- a/README.md +++ b/README.md @@ -123,21 +123,21 @@ Hutool的存在就是为了减少代码搜索成本,避免网络上参差不 cn.hutool hutool-all - 5.5.2 + 5.5.3 ``` ### Gradle ``` -compile 'cn.hutool:hutool-all:5.5.2' +compile 'cn.hutool:hutool-all:5.5.3' ``` ### 非Maven项目 点击以下任一链接,下载`hutool-all-X.X.X.jar`即可: -- [Maven中央库1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.5.2/) -- [Maven中央库2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.5.2/) +- [Maven中央库1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.5.3/) +- [Maven中央库2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/5.5.3/) > 注意 > Hutool 5.x支持JDK8+,对Android平台没有测试,不能保证所有工具类或工具方法可用。 diff --git a/bin/version.txt b/bin/version.txt index e4d41db98..d4e50692a 100755 --- a/bin/version.txt +++ b/bin/version.txt @@ -1 +1 @@ -5.5.2 +5.5.3 diff --git a/docs/js/version.js b/docs/js/version.js index 352759200..f133da001 100644 --- a/docs/js/version.js +++ b/docs/js/version.js @@ -1 +1 @@ -var version = '5.5.2' \ No newline at end of file +var version = '5.5.3' \ No newline at end of file diff --git a/hutool-all/pom.xml b/hutool-all/pom.xml index bc1974b6d..ae2daea7e 100644 --- a/hutool-all/pom.xml +++ b/hutool-all/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-all diff --git a/hutool-aop/pom.xml b/hutool-aop/pom.xml index 6072c8042..a5db73731 100644 --- a/hutool-aop/pom.xml +++ b/hutool-aop/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-aop diff --git a/hutool-bloomFilter/pom.xml b/hutool-bloomFilter/pom.xml index 90911c30b..e1ad8127b 100644 --- a/hutool-bloomFilter/pom.xml +++ b/hutool-bloomFilter/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-bloomFilter diff --git a/hutool-bom/pom.xml b/hutool-bom/pom.xml index 5761483e0..2e1791ca9 100644 --- a/hutool-bom/pom.xml +++ b/hutool-bom/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-bom diff --git a/hutool-cache/pom.xml b/hutool-cache/pom.xml index 717246ef4..d24ef2f04 100644 --- a/hutool-cache/pom.xml +++ b/hutool-cache/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-cache diff --git a/hutool-captcha/pom.xml b/hutool-captcha/pom.xml index 1188dc0b1..1ae88a98f 100644 --- a/hutool-captcha/pom.xml +++ b/hutool-captcha/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-captcha diff --git a/hutool-core/pom.xml b/hutool-core/pom.xml index 96e9ca97e..d2d3282fe 100644 --- a/hutool-core/pom.xml +++ b/hutool-core/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-core diff --git a/hutool-cron/pom.xml b/hutool-cron/pom.xml index 0e7f926c4..7ea67cc03 100644 --- a/hutool-cron/pom.xml +++ b/hutool-cron/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-cron diff --git a/hutool-crypto/pom.xml b/hutool-crypto/pom.xml index 64bbce6a5..37a05cd4c 100644 --- a/hutool-crypto/pom.xml +++ b/hutool-crypto/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-crypto diff --git a/hutool-db/pom.xml b/hutool-db/pom.xml index d04060350..b4afce592 100644 --- a/hutool-db/pom.xml +++ b/hutool-db/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-db diff --git a/hutool-dfa/pom.xml b/hutool-dfa/pom.xml index 2a7297373..61e62b9b7 100644 --- a/hutool-dfa/pom.xml +++ b/hutool-dfa/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-dfa diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml index d1fb6f0be..524fe3c25 100644 --- a/hutool-extra/pom.xml +++ b/hutool-extra/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-extra diff --git a/hutool-http/pom.xml b/hutool-http/pom.xml index 0adcaf5f5..fd3c92669 100644 --- a/hutool-http/pom.xml +++ b/hutool-http/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-http diff --git a/hutool-json/pom.xml b/hutool-json/pom.xml index c9634756a..e4b4f15e5 100644 --- a/hutool-json/pom.xml +++ b/hutool-json/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-json diff --git a/hutool-log/pom.xml b/hutool-log/pom.xml index 5b665b010..99d90c35f 100644 --- a/hutool-log/pom.xml +++ b/hutool-log/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-log diff --git a/hutool-poi/pom.xml b/hutool-poi/pom.xml index c6d46f939..8e67a85d7 100644 --- a/hutool-poi/pom.xml +++ b/hutool-poi/pom.xml @@ -8,7 +8,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-poi diff --git a/hutool-script/pom.xml b/hutool-script/pom.xml index a2c2ec316..fb5c71259 100644 --- a/hutool-script/pom.xml +++ b/hutool-script/pom.xml @@ -8,7 +8,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-script diff --git a/hutool-setting/pom.xml b/hutool-setting/pom.xml index 8657d5f30..ccedbb261 100644 --- a/hutool-setting/pom.xml +++ b/hutool-setting/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-setting diff --git a/hutool-socket/pom.xml b/hutool-socket/pom.xml index 8b723e2d1..60d88187e 100644 --- a/hutool-socket/pom.xml +++ b/hutool-socket/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-socket diff --git a/hutool-system/pom.xml b/hutool-system/pom.xml index f7eab3da3..1772b1bdd 100644 --- a/hutool-system/pom.xml +++ b/hutool-system/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool-system diff --git a/pom.xml b/pom.xml index bb8ab60c7..66c86ae69 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ cn.hutool hutool-parent - 5.5.2 + 5.5.3-SNAPSHOT hutool Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。 https://github.com/looly/hutool From d6e8e64bc5d7d6f6e1e5ae910d5657cad5a0110d Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 2 Dec 2020 17:40:03 +0800 Subject: [PATCH 02/47] prepare 5.5.3 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f33b0a485..22ea92476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ ------------------------------------------------------------------------------------------------------------- +# 5.5.3 (2020-12-02) + +### 新特性 +### Bug修复 + +------------------------------------------------------------------------------------------------------------- + # 5.5.2 (2020-12-01) ### 新特性 From cc43e395158ae9762e6aec9caaaff8582fda73ee Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 2 Dec 2020 21:16:37 +0800 Subject: [PATCH 03/47] int to long --- CHANGELOG.md | 3 +++ .../core/net/multipart/MultipartFormData.java | 7 ++----- .../MultipartRequestInputStream.java | 2 +- .../hutool/core/net/multipart/UploadFile.java | 13 ++++++------ .../core/net/multipart/UploadFileHeader.java | 3 ++- .../core/net/multipart/UploadSetting.java | 6 +++--- .../java/cn/hutool/core/util/IdcardUtil.java | 4 +++- .../java/cn/hutool/crypto/SecureUtil.java | 14 ++++++------- .../cn/hutool/crypto/digest/Digester.java | 20 +++++++++---------- 9 files changed, 37 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ea92476..46734c020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ # 5.5.3 (2020-12-02) ### 新特性 +* 【core 】 IdcardUtil增加行政区划83(issue#1277@Github) +* 【core 】 multipart中int改为long,解决大文件上传越界问题(issue#I27WZ3@Gitee) + ### Bug修复 ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java b/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java index b6162dc98..aa1ba0e79 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java @@ -2,9 +2,9 @@ package cn.hutool.core.net.multipart; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IoUtil; import cn.hutool.core.map.multi.ListValueMap; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; @@ -78,10 +78,7 @@ public class MultipartFormData { putFile(header.formFieldName, newFile); } else { // 标准表单项 - ByteArrayOutputStream fbos = new ByteArrayOutputStream(1024); - input.copy(fbos); - String value = (charset != null) ? new String(fbos.toByteArray(), charset) : new String(fbos.toByteArray()); - putParameter(header.formFieldName, value); + putParameter(header.formFieldName, IoUtil.read(input, charset)); } input.skipBytes(1); diff --git a/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java b/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java index 8dc5df6b0..08d7fbacb 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java @@ -171,7 +171,7 @@ public class MultipartRequestInputStream extends BufferedInputStream { * @return 复制的字节数 * @throws IOException 读取异常 */ - public int copy(OutputStream out, int limit) throws IOException { + public int copy(OutputStream out, long limit) throws IOException { int count = 0; while (true) { byte b = readByte(); diff --git a/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFile.java b/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFile.java index 4cc456934..3734ca11b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFile.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFile.java @@ -7,7 +7,6 @@ import cn.hutool.core.util.StrUtil; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -24,7 +23,7 @@ public class UploadFile { private final UploadFileHeader header; private final UploadSetting setting; - private int size = -1; + private long size = -1; // 文件流(小文件位于内存中) private byte[] data; @@ -150,7 +149,7 @@ public class UploadFile { /** * @return 上传文件的大小,> 0 表示未上传 */ - public int size() { + public long size() { return size; } @@ -200,13 +199,13 @@ public class UploadFile { // 处理硬盘文件 tempFile = FileUtil.createTempFile(TMP_FILE_PREFIX, TMP_FILE_SUFFIX, FileUtil.touch(setting.tmpUploadPath), false); - BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile)); + final BufferedOutputStream out = FileUtil.getOutputStream(this.tempFile); if (data != null) { size = data.length; out.write(data); data = null; // not needed anymore } - int maxFileSize = setting.maxFileSize; + final long maxFileSize = setting.maxFileSize; try { if (maxFileSize == -1) { size += input.copy(out); @@ -236,14 +235,14 @@ public class UploadFile { * @return 是否为允许的扩展名 */ private boolean isAllowedExtension() { - String[] exts = setting.fileExts; + final String[] exts = setting.fileExts; boolean isAllow = setting.isAllowFileExts; if (exts == null || exts.length == 0) { // 如果给定扩展名列表为空,当允许扩展名时全部允许,否则全部禁止 return isAllow; } - String fileNameExt = FileUtil.extName(this.getFileName()); + final String fileNameExt = FileUtil.extName(this.getFileName()); for (String fileExtension : setting.fileExts) { if (fileNameExt.equalsIgnoreCase(fileExtension)) { return isAllow; diff --git a/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFileHeader.java b/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFileHeader.java index 5c1770d9b..8f769dd97 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFileHeader.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadFileHeader.java @@ -31,7 +31,8 @@ public class UploadFileHeader { // ---------------------------------------------------------------- public interface /** - * Returns true if uploaded data are correctly marked as a file. This is true if header contains string 'filename'. + * Returns {@code true} if uploaded data are correctly marked as a file.
+ * This is true if header contains string 'filename'. * * @return 是否为文件 */ diff --git a/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadSetting.java b/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadSetting.java index 872bb7010..5af183ad6 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadSetting.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/multipart/UploadSetting.java @@ -9,7 +9,7 @@ package cn.hutool.core.net.multipart; public class UploadSetting { /** 最大文件大小,默认无限制 */ - protected int maxFileSize = -1; + protected long maxFileSize = -1; /** 文件保存到内存的边界 */ protected int memoryThreshold = 8192; /** 临时文件目录 */ @@ -26,7 +26,7 @@ public class UploadSetting { /** * @return 获得最大文件大小,-1表示无限制 */ - public int getMaxFileSize() { + public long getMaxFileSize() { return maxFileSize; } @@ -35,7 +35,7 @@ public class UploadSetting { * * @param maxFileSize 最大文件大小 */ - public void setMaxFileSize(int maxFileSize) { + public void setMaxFileSize(long maxFileSize) { this.maxFileSize = maxFileSize; } diff --git a/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java index 7fdfb75e5..4502ba103 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java @@ -83,6 +83,8 @@ public class IdcardUtil { CITY_CODES.put("71", "台湾"); CITY_CODES.put("81", "香港"); CITY_CODES.put("82", "澳门"); + //issue#1277,台湾身份证号码以83开头,但是行政区划为71 + CITY_CODES.put("83", "台湾"); CITY_CODES.put("91", "国外"); TW_FIRST_CODE.put("A", 10); @@ -354,7 +356,7 @@ public class IdcardUtil { sum = sum + Integer.parseInt(String.valueOf(c)) * iflag; iflag--; } - if ("A".equals(end.toUpperCase())) { + if ("A".equalsIgnoreCase(end)) { sum += 10; } else { sum += Integer.parseInt(end); diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/SecureUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/SecureUtil.java index 07936cddb..78f97b2df 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/SecureUtil.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/SecureUtil.java @@ -626,7 +626,7 @@ public final class SecureUtil { * 创建HMac对象,调用digest方法可获得hmac值 * * @param algorithm {@link HmacAlgorithm} - * @param key 密钥,如果为null生成随机密钥 + * @param key 密钥,如果为{@code null}生成随机密钥 * @return {@link HMac} * @since 3.3.0 */ @@ -638,7 +638,7 @@ public final class SecureUtil { * 创建HMac对象,调用digest方法可获得hmac值 * * @param algorithm {@link HmacAlgorithm} - * @param key 密钥,如果为null生成随机密钥 + * @param key 密钥,如果为{@code null}生成随机密钥 * @return {@link HMac} * @since 3.0.3 */ @@ -650,7 +650,7 @@ public final class SecureUtil { * 创建HMac对象,调用digest方法可获得hmac值 * * @param algorithm {@link HmacAlgorithm} - * @param key 密钥{@link SecretKey},如果为null生成随机密钥 + * @param key 密钥{@link SecretKey},如果为{@code null}生成随机密钥 * @return {@link HMac} * @since 3.0.3 */ @@ -664,7 +664,7 @@ public final class SecureUtil { * HmacMD5加密:hmacMd5(key).digest(data)
* HmacMD5加密并转为16进制字符串:hmacMd5(key).digestHex(data)
* - * @param key 加密密钥,如果为null生成随机密钥 + * @param key 加密密钥,如果为{@code null}生成随机密钥 * @return {@link HMac} * @since 3.3.0 */ @@ -678,7 +678,7 @@ public final class SecureUtil { * HmacMD5加密:hmacMd5(key).digest(data)
* HmacMD5加密并转为16进制字符串:hmacMd5(key).digestHex(data)
* - * @param key 加密密钥,如果为null生成随机密钥 + * @param key 加密密钥,如果为{@code null}生成随机密钥 * @return {@link HMac} */ public static HMac hmacMd5(byte[] key) { @@ -703,7 +703,7 @@ public final class SecureUtil { * HmacSHA1加密:hmacSha1(key).digest(data)
* HmacSHA1加密并转为16进制字符串:hmacSha1(key).digestHex(data)
* - * @param key 加密密钥,如果为null生成随机密钥 + * @param key 加密密钥,如果为{@code null}生成随机密钥 * @return {@link HMac} * @since 3.3.0 */ @@ -717,7 +717,7 @@ public final class SecureUtil { * HmacSHA1加密:hmacSha1(key).digest(data)
* HmacSHA1加密并转为16进制字符串:hmacSha1(key).digestHex(data)
* - * @param key 加密密钥,如果为null生成随机密钥 + * @param key 加密密钥,如果为{@code null}生成随机密钥 * @return {@link HMac} */ public static HMac hmacSha1(byte[] key) { diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java index bddc47151..eb749e080 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java @@ -1,14 +1,5 @@ package cn.hutool.crypto.digest; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; - import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; @@ -19,6 +10,15 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.CryptoException; import cn.hutool.crypto.SecureUtil; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; + /** * 摘要算法
* 注意:此对象实例化后为非线程安全! @@ -84,7 +84,7 @@ public class Digester implements Serializable { * * @param algorithm 算法 * @param provider 算法提供者,null表示JDK默认,可以引入Bouncy Castle等来提供更多算法支持 - * @return {@link Digester} + * @return Digester * @throws CryptoException Cause by IOException */ public Digester init(String algorithm, Provider provider) { From 6c76e9258b7388b4ccf3d37a72c446d24604da5e Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 2 Dec 2020 22:21:21 +0800 Subject: [PATCH 04/47] change test --- .../core/io/FastByteArrayOutputStream.java | 8 +++-- .../core/net/multipart/MultipartFormData.java | 3 +- .../MultipartRequestInputStream.java | 17 +++++++++- .../hutool/http/{test => }/DownloadTest.java | 2 +- .../hutool/http/{test => }/HtmlUtilTest.java | 2 +- .../http/{test => }/HttpRequestTest.java | 2 +- .../hutool/http/{test => }/HttpUtilTest.java | 2 +- .../cn/hutool/http/{test => }/HttpsTest.java | 2 +- .../cn/hutool/http/{test => }/RestTest.java | 2 +- .../cn/hutool/http/{test => }/UploadTest.java | 34 +++++++++++++++---- 10 files changed, 55 insertions(+), 19 deletions(-) rename hutool-http/src/test/java/cn/hutool/http/{test => }/DownloadTest.java (95%) rename hutool-http/src/test/java/cn/hutool/http/{test => }/HtmlUtilTest.java (96%) rename hutool-http/src/test/java/cn/hutool/http/{test => }/HttpRequestTest.java (95%) rename hutool-http/src/test/java/cn/hutool/http/{test => }/HttpUtilTest.java (96%) rename hutool-http/src/test/java/cn/hutool/http/{test => }/HttpsTest.java (91%) rename hutool-http/src/test/java/cn/hutool/http/{test => }/RestTest.java (94%) rename hutool-http/src/test/java/cn/hutool/http/{test => }/UploadTest.java (58%) diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java b/hutool-core/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java index a27cace9d..088db9ee0 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java @@ -1,6 +1,7 @@ package cn.hutool.core.io; import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; import java.io.IOException; import java.io.OutputStream; @@ -90,7 +91,7 @@ public class FastByteArrayOutputStream extends OutputStream { @Override public String toString() { - return new String(toByteArray()); + return toString(CharsetUtil.defaultCharset()); } /** @@ -104,11 +105,12 @@ public class FastByteArrayOutputStream extends OutputStream { /** * 转为字符串 - * @param charset 编码 + * @param charset 编码,null表示默认编码 * @return 字符串 */ public String toString(Charset charset) { - return new String(toByteArray(), charset); + return new String(toByteArray(), + ObjectUtil.defaultIfNull(charset, CharsetUtil.defaultCharset())); } } \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java b/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java index aa1ba0e79..76a12178f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartFormData.java @@ -2,7 +2,6 @@ package cn.hutool.core.net.multipart; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; -import cn.hutool.core.io.IoUtil; import cn.hutool.core.map.multi.ListValueMap; import java.io.IOException; @@ -78,7 +77,7 @@ public class MultipartFormData { putFile(header.formFieldName, newFile); } else { // 标准表单项 - putParameter(header.formFieldName, IoUtil.read(input, charset)); + putParameter(header.formFieldName, input.readString(charset)); } input.skipBytes(1); diff --git a/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java b/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java index 08d7fbacb..12acf6b6e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/multipart/MultipartRequestInputStream.java @@ -1,5 +1,7 @@ package cn.hutool.core.net.multipart; +import cn.hutool.core.io.FastByteArrayOutputStream; + import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -144,7 +146,20 @@ public class MultipartRequestInputStream extends BufferedInputStream { // ---------------------------------------------------------------- copy /** - * 全部字节流复制到out + * 读取字节流,直到下一个boundary + * + * @param charset 编码,null表示系统默认编码 + * @return 读取的字符串 + * @throws IOException 读取异常 + */ + public String readString(Charset charset) throws IOException { + final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + copy(out); + return out.toString(charset); + } + + /** + * 字节流复制到out,直到下一个boundary * * @param out 输出流 * @return 复制的字节数 diff --git a/hutool-http/src/test/java/cn/hutool/http/test/DownloadTest.java b/hutool-http/src/test/java/cn/hutool/http/DownloadTest.java similarity index 95% rename from hutool-http/src/test/java/cn/hutool/http/test/DownloadTest.java rename to hutool-http/src/test/java/cn/hutool/http/DownloadTest.java index 777ca9495..823f01d0c 100644 --- a/hutool-http/src/test/java/cn/hutool/http/test/DownloadTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/DownloadTest.java @@ -1,4 +1,4 @@ -package cn.hutool.http.test; +package cn.hutool.http; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IORuntimeException; diff --git a/hutool-http/src/test/java/cn/hutool/http/test/HtmlUtilTest.java b/hutool-http/src/test/java/cn/hutool/http/HtmlUtilTest.java similarity index 96% rename from hutool-http/src/test/java/cn/hutool/http/test/HtmlUtilTest.java rename to hutool-http/src/test/java/cn/hutool/http/HtmlUtilTest.java index 66eb9421e..2103b71ce 100644 --- a/hutool-http/src/test/java/cn/hutool/http/test/HtmlUtilTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/HtmlUtilTest.java @@ -1,4 +1,4 @@ -package cn.hutool.http.test; +package cn.hutool.http; import org.junit.Assert; import org.junit.Test; diff --git a/hutool-http/src/test/java/cn/hutool/http/test/HttpRequestTest.java b/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java similarity index 95% rename from hutool-http/src/test/java/cn/hutool/http/test/HttpRequestTest.java rename to hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java index bf00b18ff..2dc9e487d 100644 --- a/hutool-http/src/test/java/cn/hutool/http/test/HttpRequestTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java @@ -1,4 +1,4 @@ -package cn.hutool.http.test; +package cn.hutool.http; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; diff --git a/hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java b/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java similarity index 96% rename from hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java rename to hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java index 201605cae..3cc5d25c1 100644 --- a/hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java @@ -1,4 +1,4 @@ -package cn.hutool.http.test; +package cn.hutool.http; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Console; diff --git a/hutool-http/src/test/java/cn/hutool/http/test/HttpsTest.java b/hutool-http/src/test/java/cn/hutool/http/HttpsTest.java similarity index 91% rename from hutool-http/src/test/java/cn/hutool/http/test/HttpsTest.java rename to hutool-http/src/test/java/cn/hutool/http/HttpsTest.java index fca3e6cc1..a2eef2416 100644 --- a/hutool-http/src/test/java/cn/hutool/http/test/HttpsTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/HttpsTest.java @@ -1,4 +1,4 @@ -package cn.hutool.http.test; +package cn.hutool.http; import cn.hutool.core.lang.Console; import cn.hutool.core.thread.ThreadUtil; diff --git a/hutool-http/src/test/java/cn/hutool/http/test/RestTest.java b/hutool-http/src/test/java/cn/hutool/http/RestTest.java similarity index 94% rename from hutool-http/src/test/java/cn/hutool/http/test/RestTest.java rename to hutool-http/src/test/java/cn/hutool/http/RestTest.java index 4a43c05c6..100d4e201 100644 --- a/hutool-http/src/test/java/cn/hutool/http/test/RestTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/RestTest.java @@ -1,4 +1,4 @@ -package cn.hutool.http.test; +package cn.hutool.http; import cn.hutool.core.lang.Console; import cn.hutool.http.Header; diff --git a/hutool-http/src/test/java/cn/hutool/http/test/UploadTest.java b/hutool-http/src/test/java/cn/hutool/http/UploadTest.java similarity index 58% rename from hutool-http/src/test/java/cn/hutool/http/test/UploadTest.java rename to hutool-http/src/test/java/cn/hutool/http/UploadTest.java index 6a8e151bd..e0817c180 100644 --- a/hutool-http/src/test/java/cn/hutool/http/test/UploadTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/UploadTest.java @@ -1,23 +1,23 @@ -package cn.hutool.http.test; +package cn.hutool.http; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Console; -import cn.hutool.http.HttpRequest; -import cn.hutool.http.HttpResponse; -import cn.hutool.http.HttpUtil; import org.junit.Ignore; import org.junit.Test; import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; +import java.util.Map; /** * 文件上传单元测试 - * @author looly * + * @author looly */ public class UploadTest { - + /** * 多文件上传测试 */ @@ -35,7 +35,7 @@ public class UploadTest { HttpResponse response = request.execute(); Console.log(response.body()); } - + @Test @Ignore public void uploadFileTest() { @@ -48,4 +48,24 @@ public class UploadTest { String result = HttpUtil.post("http://wthrcdn.etouch.cn/weather_mini", paramMap); System.out.println(result); } + + @Test + @Ignore + public void uploadTest() { + //客户端 + String url = "http://localhost:8888/file"; + Path file = Paths.get("D:\\testBigData_upload.xlsx"); + Map headers = new HashMap<>(16); + headers.put("md5", "aaaaaaaa"); + + Map params = new HashMap<>(16); + params.put("fileName", file.toFile().getName()); + params.put("file", file.toFile()); + HttpRequest httpRequest = HttpRequest.post(url) + .setChunkedStreamingMode(1024 * 1024) + .headerMap(headers, false) + .form(params); + HttpResponse httpResponse = httpRequest.execute(); + Console.log(httpResponse); + } } From 1c9d06979b98cd11a2a805161fd30c375694846e Mon Sep 17 00:00:00 2001 From: huanhu01 Date: Thu, 3 Dec 2020 20:24:26 +0800 Subject: [PATCH 05/47] fix out of index --- .../src/main/java/cn/hutool/core/collection/ListUtil.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java index 98bf9645d..37d208f98 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java @@ -261,6 +261,9 @@ public class ListUtil { final int[] startEnd = PageUtil.transToStartEnd(pageNo, pageSize); if (startEnd[1] > resultSize) { startEnd[1] = resultSize; + if (startEnd[0] > startEnd[1]) { + return new ArrayList<>(0); + } } return list.subList(startEnd[0], startEnd[1]); From 8d81a5966644a54ed4967f169cddf7db496e5045 Mon Sep 17 00:00:00 2001 From: Looly Date: Fri, 4 Dec 2020 09:42:58 +0800 Subject: [PATCH 06/47] fix cache bug --- CHANGELOG.md | 1 + .../java/cn/hutool/cache/impl/AbstractCache.java | 12 +++++------- .../main/java/cn/hutool/db/handler/RsHandler.java | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46734c020..3816909cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * 【core 】 multipart中int改为long,解决大文件上传越界问题(issue#I27WZ3@Gitee) ### Bug修复 +* 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java index fa54d16ba..f5e583f82 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java @@ -15,7 +15,7 @@ import java.util.concurrent.locks.StampedLock; * 继承此抽象缓存需要:
*
    *
  • 创建一个新的Map
  • - *
  • 实现 prune 策略
  • + *
  • 实现 {@code prune} 策略
  • *
* * @param 键类型 @@ -30,11 +30,11 @@ public abstract class AbstractCache implements Cache { private final StampedLock lock = new StampedLock(); /** - * 返回缓存容量,0表示无大小限制 + * 返回缓存容量,{@code 0}表示无大小限制 */ protected int capacity; /** - * 缓存失效时长, 0 表示无限制,单位毫秒 + * 缓存失效时长, {@code 0} 表示无限制,单位毫秒 */ protected long timeout; @@ -168,10 +168,8 @@ public abstract class AbstractCache implements Cache { return null; } - if (co.isExpired()) { - missCount.getAndIncrement(); - } else { - // 命中 + // 命中 + if (false == co.isExpired()) { hitCount.getAndIncrement(); return co.get(isUpdateLastAccess); } diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/RsHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/RsHandler.java index dcc9a3178..e595613ee 100644 --- a/hutool-db/src/main/java/cn/hutool/db/handler/RsHandler.java +++ b/hutool-db/src/main/java/cn/hutool/db/handler/RsHandler.java @@ -19,6 +19,7 @@ import java.sql.SQLException; * @author Luxiaolei * */ +@FunctionalInterface public interface RsHandler extends Serializable{ /** From 52d341e650fda6e1b765b1d49cf4db91444bc998 Mon Sep 17 00:00:00 2001 From: Looly Date: Fri, 4 Dec 2020 11:09:45 +0800 Subject: [PATCH 07/47] add check --- CHANGELOG.md | 3 ++- .../src/main/java/cn/hutool/core/collection/ListUtil.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3816909cb..2f6504912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,12 @@ ------------------------------------------------------------------------------------------------------------- -# 5.5.3 (2020-12-02) +# 5.5.3 (2020-12-04) ### 新特性 * 【core 】 IdcardUtil增加行政区划83(issue#1277@Github) * 【core 】 multipart中int改为long,解决大文件上传越界问题(issue#I27WZ3@Gitee) +* 【core 】 ListUtil.page增加检查(pr#224@Gitee) ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java index 37d208f98..b3bffe1c9 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/ListUtil.java @@ -245,7 +245,7 @@ public class ListUtil { int resultSize = list.size(); // 每页条目数大于总数直接返回所有 if (resultSize <= pageSize) { - if (pageNo < (PageUtil.getFirstPageNo()+1)) { + if (pageNo < (PageUtil.getFirstPageNo() + 1)) { return Collections.unmodifiableList(list); } else { // 越界直接返回空 @@ -253,7 +253,7 @@ public class ListUtil { } } // 相乘可能会导致越界 临时用long - if (((long) (pageNo-PageUtil.getFirstPageNo()) * pageSize) > resultSize) { + if (((long) (pageNo - PageUtil.getFirstPageNo()) * pageSize) > resultSize) { // 越界直接返回空 return new ArrayList<>(0); } @@ -262,7 +262,7 @@ public class ListUtil { if (startEnd[1] > resultSize) { startEnd[1] = resultSize; if (startEnd[0] > startEnd[1]) { - return new ArrayList<>(0); + return empty(); } } From afe5844b2508792863d69e6a3635bad655e8e6b6 Mon Sep 17 00:00:00 2001 From: LazzMan <17089793114@163.com> Date: Fri, 4 Dec 2020 16:41:48 +0800 Subject: [PATCH 08/47] NumberUtil add new method --- .../java/cn/hutool/core/util/NumberUtil.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java index 96f17de45..f7de88e65 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java @@ -2493,6 +2493,58 @@ public class NumberUtil { return true; } + /** + * 检查value是否在[min,max]范围内 + * + * @param min 最小值 + * @param max 最大值 + * @param value 被检查值 + * @return 检查结果,范围内将返回true,否则返回false + * @since 5.5.4 + */ + public static boolean isBetween(int min, int max, int value) { + return value >= min && value <= max; + } + + /** + * 检查value是否在[min,max]范围内 + * + * @param min 最小值 + * @param max 最大值 + * @param value 被检查值 + * @return 检查结果,范围内将返回true,否则返回false + * @since 5.5.4 + */ + public static boolean isBetween(long min, long max, long value) { + return value >= min && value <= max; + } + + /** + * 检查value是否在[min,max]范围内 + * + * @param min 最小值 + * @param max 最大值 + * @param value 被检查值 + * @return 检查结果,范围内将返回true,否则返回false + * @since 5.5.4 + */ + public static boolean isBetween(float min, float max, float value) { + return value >= min && value <= max; + } + + /** + * 检查value是否在[min,max]范围内 + * + * @param min 最小值 + * @param max 最大值 + * @param value 被检查值 + * @return 检查结果,范围内将返回true,否则返回false + * @since 5.5.4 + */ + public static boolean isBetween(double min, double max, double value) { + return value >= min && value <= max; + } + // ------------------------------------------------------------------------------------------- Private method start private static int mathSubnode(int selectNum, int minNum) { if (selectNum == minNum) { From 626f4e51d0561970ec4e95bb2ddbe60f623de1b9 Mon Sep 17 00:00:00 2001 From: LazzMan <17089793114@163.com> Date: Fri, 4 Dec 2020 16:56:25 +0800 Subject: [PATCH 09/47] NumberUtil add unit-test --- .../src/test/java/cn/hutool/core/util/NumberUtilTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java index cbd632a0f..5db2db167 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java @@ -303,4 +303,12 @@ public class NumberUtilTest { Assert.assertEquals("0", NumberUtil.toStr(NumberUtil.sub(new BigDecimal("9600.0000000000"), new BigDecimal("9600.000000")))); Assert.assertEquals("0", NumberUtil.toStr(new BigDecimal("9600.00000").subtract(new BigDecimal("9600.000000000")))); } + + @Test + public void isBetweenTest() { + Assert.assertTrue(NumberUtil.isBetween(0, 1, 0)); + Assert.assertTrue(NumberUtil.isBetween(0l, 1l, 1l)); + Assert.assertTrue(NumberUtil.isBetween(0.1f, 0.2f, 0.19f)); + Assert.assertTrue(NumberUtil.isBetween(0.1, 0.2, 0.19)); + } } From 9ad2848bbd718bbce25ea671bb9b9713bf7ac928 Mon Sep 17 00:00:00 2001 From: Looly Date: Sat, 5 Dec 2020 16:05:36 +0800 Subject: [PATCH 10/47] fix code --- CHANGELOG.md | 2 +- .../java/cn/hutool/core/util/NumberUtil.java | 58 +------------------ .../cn/hutool/core/lang/ValidatorTest.java | 8 +++ .../cn/hutool/core/util/NumberUtilTest.java | 8 --- 4 files changed, 12 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f6504912..976b6982c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------------------------------------------- -# 5.5.3 (2020-12-04) +# 5.5.3 (2020-12-05) ### 新特性 * 【core 】 IdcardUtil增加行政区划83(issue#1277@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java index f7de88e65..a300dc01e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java @@ -1516,7 +1516,7 @@ public class NumberUtil { */ public static int processMultiple(int selectNum, int minNum) { int result; - result = mathSubnode(selectNum, minNum) / mathNode(selectNum - minNum); + result = mathSubNode(selectNum, minNum) / mathNode(selectNum - minNum); return result; } @@ -2493,64 +2493,12 @@ public class NumberUtil { return true; } - /** - * 检查value是否在[min,max]范围内 - * - * @param min 最小值 - * @param max 最大值 - * @param value 被检查值 - * @return 检查结果,范围内将返回true,否则返回false - * @since 5.5.4 - */ - public static boolean isBetween(int min, int max, int value) { - return value >= min && value <= max; - } - - /** - * 检查value是否在[min,max]范围内 - * - * @param min 最小值 - * @param max 最大值 - * @param value 被检查值 - * @return 检查结果,范围内将返回true,否则返回false - * @since 5.5.4 - */ - public static boolean isBetween(long min, long max, long value) { - return value >= min && value <= max; - } - - /** - * 检查value是否在[min,max]范围内 - * - * @param min 最小值 - * @param max 最大值 - * @param value 被检查值 - * @return 检查结果,范围内将返回true,否则返回false - * @since 5.5.4 - */ - public static boolean isBetween(float min, float max, float value) { - return value >= min && value <= max; - } - - /** - * 检查value是否在[min,max]范围内 - * - * @param min 最小值 - * @param max 最大值 - * @param value 被检查值 - * @return 检查结果,范围内将返回true,否则返回false - * @since 5.5.4 - */ - public static boolean isBetween(double min, double max, double value) { - return value >= min && value <= max; - } - // ------------------------------------------------------------------------------------------- Private method start - private static int mathSubnode(int selectNum, int minNum) { + private static int mathSubNode(int selectNum, int minNum) { if (selectNum == minNum) { return 1; } else { - return selectNum * mathSubnode(selectNum - 1, minNum); + return selectNum * mathSubNode(selectNum - 1, minNum); } } diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java index 3e240709d..101c53db8 100644 --- a/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java @@ -187,4 +187,12 @@ public class ValidatorTest { zipCode = Validator.isZipCode("102629"); Assert.assertTrue(zipCode); } + + @Test + public void isBetweenTest() { + Assert.assertTrue(Validator.isBetween(0, 0, 1)); + Assert.assertTrue(Validator.isBetween(1L, 0L, 1L)); + Assert.assertTrue(Validator.isBetween(0.19f, 0.1f, 0.2f)); + Assert.assertTrue(Validator.isBetween(0.19, 0.1, 0.2)); + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java index 5db2db167..cbd632a0f 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java @@ -303,12 +303,4 @@ public class NumberUtilTest { Assert.assertEquals("0", NumberUtil.toStr(NumberUtil.sub(new BigDecimal("9600.0000000000"), new BigDecimal("9600.000000")))); Assert.assertEquals("0", NumberUtil.toStr(new BigDecimal("9600.00000").subtract(new BigDecimal("9600.000000000")))); } - - @Test - public void isBetweenTest() { - Assert.assertTrue(NumberUtil.isBetween(0, 1, 0)); - Assert.assertTrue(NumberUtil.isBetween(0l, 1l, 1l)); - Assert.assertTrue(NumberUtil.isBetween(0.1f, 0.2f, 0.19f)); - Assert.assertTrue(NumberUtil.isBetween(0.1, 0.2, 0.19)); - } } From 7103adc02e4951ca8360b6d34b499018528ec6c2 Mon Sep 17 00:00:00 2001 From: Looly Date: Sat, 5 Dec 2020 16:37:49 +0800 Subject: [PATCH 11/47] change packge and add date format support --- .../poi/excel/sax/Excel07SaxReader.java | 16 ++++++++-------- .../cn/hutool/poi/excel/sax/ExcelSaxUtil.java | 19 +++++++++++++++++++ .../excel/{test => }/BigExcelWriteTest.java | 0 .../poi/excel/{test => }/CellUtilTest.java | 0 .../poi/excel/{test => }/ExcelReadTest.java | 0 .../excel/{test => }/ExcelSaxReadTest.java | 2 +- .../poi/excel/{test => }/ExcelUtilTest.java | 0 .../poi/excel/{test => }/ExcelWriteTest.java | 0 .../poi/excel/{test => }/OrderExcel.java | 0 .../hutool/poi/excel/{test => }/TestBean.java | 0 .../poi/word/{test => }/WordWriterTest.java | 0 11 files changed, 28 insertions(+), 9 deletions(-) rename hutool-poi/src/test/java/cn/hutool/poi/excel/{test => }/BigExcelWriteTest.java (100%) rename hutool-poi/src/test/java/cn/hutool/poi/excel/{test => }/CellUtilTest.java (100%) rename hutool-poi/src/test/java/cn/hutool/poi/excel/{test => }/ExcelReadTest.java (100%) rename hutool-poi/src/test/java/cn/hutool/poi/excel/{test => }/ExcelSaxReadTest.java (93%) rename hutool-poi/src/test/java/cn/hutool/poi/excel/{test => }/ExcelUtilTest.java (100%) rename hutool-poi/src/test/java/cn/hutool/poi/excel/{test => }/ExcelWriteTest.java (100%) rename hutool-poi/src/test/java/cn/hutool/poi/excel/{test => }/OrderExcel.java (100%) rename hutool-poi/src/test/java/cn/hutool/poi/excel/{test => }/TestBean.java (100%) rename hutool-poi/src/test/java/cn/hutool/poi/word/{test => }/WordWriterTest.java (100%) diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java index beb254621..2fb116468 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java @@ -4,6 +4,7 @@ import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; import cn.hutool.core.text.StrBuilder; import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.poi.excel.cell.FormulaCellValue; import cn.hutool.poi.excel.sax.handler.RowHandler; @@ -410,21 +411,20 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader m月d日 + // 31 -> yyyy年m月d日 + return true; + } return org.apache.poi.ss.usermodel.DateUtil.isADateFormat(formatIndex, formatString); } diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/BigExcelWriteTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/BigExcelWriteTest.java similarity index 100% rename from hutool-poi/src/test/java/cn/hutool/poi/excel/test/BigExcelWriteTest.java rename to hutool-poi/src/test/java/cn/hutool/poi/excel/BigExcelWriteTest.java diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/CellUtilTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/CellUtilTest.java similarity index 100% rename from hutool-poi/src/test/java/cn/hutool/poi/excel/test/CellUtilTest.java rename to hutool-poi/src/test/java/cn/hutool/poi/excel/CellUtilTest.java diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelReadTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelReadTest.java similarity index 100% rename from hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelReadTest.java rename to hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelReadTest.java diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelSaxReadTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java similarity index 93% rename from hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelSaxReadTest.java rename to hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java index c1319182a..ffce24266 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelSaxReadTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java @@ -65,7 +65,7 @@ public class ExcelSaxReadTest { @Test @Ignore public void readBySaxTest2() { - ExcelUtil.readBySax("e:/B23_20180404164901240.xlsx", 2, (sheetIndex, rowIndex, rowList) -> Console.log(rowList)); + ExcelUtil.readBySax("d:/test/default.xlsx", -1, (sheetIndex, rowIndex, rowList) -> Console.log(rowList)); } private RowHandler createRowHandler() { diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelUtilTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelUtilTest.java similarity index 100% rename from hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelUtilTest.java rename to hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelUtilTest.java diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelWriteTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelWriteTest.java similarity index 100% rename from hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelWriteTest.java rename to hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelWriteTest.java diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/OrderExcel.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/OrderExcel.java similarity index 100% rename from hutool-poi/src/test/java/cn/hutool/poi/excel/test/OrderExcel.java rename to hutool-poi/src/test/java/cn/hutool/poi/excel/OrderExcel.java diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/TestBean.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/TestBean.java similarity index 100% rename from hutool-poi/src/test/java/cn/hutool/poi/excel/test/TestBean.java rename to hutool-poi/src/test/java/cn/hutool/poi/excel/TestBean.java diff --git a/hutool-poi/src/test/java/cn/hutool/poi/word/test/WordWriterTest.java b/hutool-poi/src/test/java/cn/hutool/poi/word/WordWriterTest.java similarity index 100% rename from hutool-poi/src/test/java/cn/hutool/poi/word/test/WordWriterTest.java rename to hutool-poi/src/test/java/cn/hutool/poi/word/WordWriterTest.java From 897dea0b30ca7ae77e0f08537cbb0789bc07d7be Mon Sep 17 00:00:00 2001 From: Looly Date: Sat, 5 Dec 2020 17:13:05 +0800 Subject: [PATCH 12/47] fix date read for sax bug --- CHANGELOG.md | 1 + .../poi/excel/sax/Excel03SaxReader.java | 2 +- .../cn/hutool/poi/excel/sax/ExcelSaxUtil.java | 76 ++++++++++-------- .../hutool/poi/excel/BigExcelWriteTest.java | 11 +-- .../cn/hutool/poi/excel/CellUtilTest.java | 2 +- .../cn/hutool/poi/excel/ExcelReadTest.java | 4 +- .../cn/hutool/poi/excel/ExcelSaxReadTest.java | 29 +++++-- .../cn/hutool/poi/excel/ExcelUtilTest.java | 3 +- .../cn/hutool/poi/excel/ExcelWriteTest.java | 16 ++-- .../java/cn/hutool/poi/excel/OrderExcel.java | 2 +- .../java/cn/hutool/poi/excel/TestBean.java | 2 +- .../cn/hutool/poi/word/WordWriterTest.java | 2 +- .../src/test/resources/data_for_sax_test.xls | Bin 31232 -> 31744 bytes .../src/test/resources/data_for_sax_test.xlsx | Bin 10000 -> 10176 bytes 14 files changed, 83 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 976b6982c..fdd19a094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) +* 【poi 】 修复sax读取自定义格式单元格无法识别日期类型的问题(issue#1283@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java index 67d31b67f..dbd97dd04 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java @@ -268,7 +268,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader createSaxReader(boolean isXlsx, RowHandler rowHandler) { + return isXlsx + ? new Excel07SaxReader(rowHandler) + : new Excel03SaxReader(rowHandler); + } + /** * 根据数据类型获取数据 * @@ -175,12 +190,13 @@ public class ExcelSaxUtil { /** * 判断数字Record中是否为日期格式 - * @param cell 单元格记录 + * + * @param cell 单元格记录 * @param formatListener {@link FormatTrackingHSSFListener} * @return 是否为日期格式 * @since 5.4.8 */ - public static boolean isDateFormat(CellValueRecordInterface cell, FormatTrackingHSSFListener formatListener){ + public static boolean isDateFormat(CellValueRecordInterface cell, FormatTrackingHSSFListener formatListener) { final int formatIndex = formatListener.getFormatIndex(cell); final String formatString = formatListener.getFormatString(cell); return isDateFormat(formatIndex, formatString); @@ -189,15 +205,15 @@ public class ExcelSaxUtil { /** * 判断日期格式 * - * @param formatIndex 格式索引,一般用于内建格式 + * @param formatIndex 格式索引,一般用于内建格式 * @param formatString 格式字符串 * @return 是否为日期格式 * @since 5.5.3 */ - public static boolean isDateFormat(int formatIndex, String formatString){ + public static boolean isDateFormat(int formatIndex, String formatString) { // https://blog.csdn.net/u014342130/article/details/50619503 // issue#1283@Github - if(formatIndex == 28 || formatIndex == 31){ + if (formatIndex == 28 || formatIndex == 31) { // 28 -> m月d日 // 31 -> yyyy年m月d日 return true; @@ -227,43 +243,22 @@ public class ExcelSaxUtil { return DateUtil.date(org.apache.poi.ss.usermodel.DateUtil.getJavaDate(value, false)); } - /** - * 创建 {@link ExcelSaxReader} - * - * @param isXlsx 是否为xlsx格式(07格式) - * @param rowHandler 行处理器 - * @return {@link ExcelSaxReader} - * @since 5.4.4 - */ - public static ExcelSaxReader createSaxReader(boolean isXlsx, RowHandler rowHandler) { - return isXlsx - ? new Excel07SaxReader(rowHandler) - : new Excel03SaxReader(rowHandler); - } - /** * 在Excel03 sax读取中获取日期或数字类型的结果值 - * @param cell 记录单元格 - * @param value 值 + * + * @param cell 记录单元格 + * @param value 值 * @param formatListener {@link FormatTrackingHSSFListener} * @return 值,可能为Date或Double或Long * @since 5.5.0 */ - public static Object getNumberOrDateValue(CellValueRecordInterface cell, double value, FormatTrackingHSSFListener formatListener){ + public static Object getNumberOrDateValue(CellValueRecordInterface cell, double value, FormatTrackingHSSFListener formatListener) { Object result; - if(ExcelSaxUtil.isDateFormat(cell, formatListener)){ + if (isDateFormat(cell, formatListener)) { // 可能为日期格式 - result = ExcelSaxUtil.getDateValue(value); - } else { - final long longPart = (long) value; - // 对于无小数部分的数字类型,转为Long,否则保留原数字 - if (((double) longPart) == value) { - result = longPart; - } else { - result = value; - } + return getDateValue(value); } - return result; + return getNumberValue(value, formatListener.getFormatString(cell)); } /** @@ -278,9 +273,20 @@ public class ExcelSaxUtil { if (StrUtil.isBlank(value)) { return null; } - double numValue = Double.parseDouble(value); + return getNumberValue(Double.parseDouble(value), numFmtString); + } + + /** + * 获取数字类型值,除非格式中明确数字保留小数,否则无小数情况下按照long返回 + * + * @param numValue 值 + * @param numFmtString 格式 + * @return 数字,可以是Double、Long + * @since 5.5.3 + */ + private static Number getNumberValue(double numValue, String numFmtString) { // 普通数字 - if (null != numFmtString && numFmtString.indexOf(StrUtil.C_DOT) < 0) { + if (null != numFmtString && false == StrUtil.contains(numFmtString, CharUtil.DOT)) { final long longPart = (long) numValue; //noinspection RedundantIfStatement if (longPart == numValue) { diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/BigExcelWriteTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/BigExcelWriteTest.java index 6cf85898d..1b5cf41ed 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/BigExcelWriteTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/BigExcelWriteTest.java @@ -1,13 +1,10 @@ -package cn.hutool.poi.excel.test; +package cn.hutool.poi.excel; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; -import cn.hutool.poi.excel.BigExcelWriter; -import cn.hutool.poi.excel.ExcelUtil; -import cn.hutool.poi.excel.ExcelWriter; import cn.hutool.poi.excel.style.StyleUtil; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.FillPatternType; @@ -163,21 +160,21 @@ public class BigExcelWriteTest { @Test @Ignore public void writeBeanTest() { - TestBean bean1 = new TestBean(); + cn.hutool.poi.excel.TestBean bean1 = new cn.hutool.poi.excel.TestBean(); bean1.setName("张三"); bean1.setAge(22); bean1.setPass(true); bean1.setScore(66.30); bean1.setExamDate(DateUtil.date()); - TestBean bean2 = new TestBean(); + cn.hutool.poi.excel.TestBean bean2 = new cn.hutool.poi.excel.TestBean(); bean2.setName("李四"); bean2.setAge(28); bean2.setPass(false); bean2.setScore(38.50); bean2.setExamDate(DateUtil.date()); - List rows = CollUtil.newArrayList(bean1, bean2); + List rows = CollUtil.newArrayList(bean1, bean2); // 通过工具类创建writer String file = "e:/bigWriteBeanTest.xlsx"; FileUtil.del(file); diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/CellUtilTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/CellUtilTest.java index 63a4cac69..11f11f896 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/CellUtilTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/CellUtilTest.java @@ -1,4 +1,4 @@ -package cn.hutool.poi.excel.test; +package cn.hutool.poi.excel; import org.apache.poi.ss.usermodel.BuiltinFormats; import org.junit.Ignore; diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelReadTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelReadTest.java index 1ed8526e1..ea6ba57a3 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelReadTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelReadTest.java @@ -1,11 +1,9 @@ -package cn.hutool.poi.excel.test; +package cn.hutool.poi.excel; import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.lang.Console; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; -import cn.hutool.poi.excel.ExcelReader; -import cn.hutool.poi.excel.ExcelUtil; import lombok.Data; import org.junit.Assert; import org.junit.Ignore; diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java index ffce24266..3407be109 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java @@ -1,4 +1,4 @@ -package cn.hutool.poi.excel.test; +package cn.hutool.poi.excel; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; @@ -6,7 +6,6 @@ import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.lang.Console; import cn.hutool.core.util.StrUtil; -import cn.hutool.poi.excel.ExcelUtil; import cn.hutool.poi.excel.cell.FormulaCellValue; import cn.hutool.poi.excel.sax.Excel03SaxReader; import cn.hutool.poi.excel.sax.handler.RowHandler; @@ -138,15 +137,33 @@ public class ExcelSaxReadTest { } @Test - public void dateReadTest() { + public void dateReadXlsTest() { List rows = new ArrayList<>(); - ExcelUtil.readBySax("data_for_sax_test.xls", 0, (i, i1, list) -> - rows.add(StrUtil.toString(list.get(0)))); + ExcelUtil.readBySax("data_for_sax_test.xls", 0, + (i, i1, list) ->{ + rows.add(StrUtil.toString(list.get(0))); + } + ); Assert.assertEquals("2020-10-09 00:00:00", rows.get(1)); // 非日期格式不做转换 Assert.assertEquals("112233", rows.get(2)); - Assert.assertEquals("1000", rows.get(3)); + Assert.assertEquals("1000.0", rows.get(3)); + Assert.assertEquals("2012-12-21 00:00:00", rows.get(4)); + } + + @Test + public void dateReadXlsxTest() { + List rows = new ArrayList<>(); + ExcelUtil.readBySax("data_for_sax_test.xlsx", 0, + (i, i1, list) -> rows.add(StrUtil.toString(list.get(0))) + ); + + Assert.assertEquals("2020-10-09 00:00:00", rows.get(1)); + // 非日期格式不做转换 + Assert.assertEquals("112233", rows.get(2)); + Assert.assertEquals("1000.0", rows.get(3)); + Assert.assertEquals("2012-12-21 00:00:00", rows.get(4)); } @Test diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelUtilTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelUtilTest.java index 8d723f9bf..0a0a7d1ca 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelUtilTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelUtilTest.java @@ -1,6 +1,5 @@ -package cn.hutool.poi.excel.test; +package cn.hutool.poi.excel; -import cn.hutool.poi.excel.ExcelUtil; import cn.hutool.poi.excel.cell.CellLocation; import org.junit.Assert; import org.junit.Test; diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelWriteTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelWriteTest.java index 17bc26ba7..4d16c964d 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelWriteTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelWriteTest.java @@ -1,4 +1,4 @@ -package cn.hutool.poi.excel.test; +package cn.hutool.poi.excel; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; @@ -6,8 +6,6 @@ import cn.hutool.core.io.FileUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; -import cn.hutool.poi.excel.ExcelUtil; -import cn.hutool.poi.excel.ExcelWriter; import cn.hutool.poi.excel.style.StyleUtil; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.FillPatternType; @@ -340,21 +338,21 @@ public class ExcelWriteTest { @Test @Ignore public void writeBeanTest() { - TestBean bean1 = new TestBean(); + cn.hutool.poi.excel.TestBean bean1 = new cn.hutool.poi.excel.TestBean(); bean1.setName("张三"); bean1.setAge(22); bean1.setPass(true); bean1.setScore(66.30); bean1.setExamDate(DateUtil.date()); - TestBean bean2 = new TestBean(); + cn.hutool.poi.excel.TestBean bean2 = new cn.hutool.poi.excel.TestBean(); bean2.setName("李四"); bean2.setAge(28); bean2.setPass(false); bean2.setScore(38.50); bean2.setExamDate(DateUtil.date()); - List rows = CollUtil.newArrayList(bean1, bean2); + List rows = CollUtil.newArrayList(bean1, bean2); // 通过工具类创建writer String file = "e:/writeBeanTest.xlsx"; FileUtil.del(file); @@ -376,17 +374,17 @@ public class ExcelWriteTest { @Test @Ignore public void writeBeanTest2() { - OrderExcel order1 = new OrderExcel(); + cn.hutool.poi.excel.OrderExcel order1 = new cn.hutool.poi.excel.OrderExcel(); order1.setId("1"); order1.setNum("123"); order1.setBody("body1"); - OrderExcel order2 = new OrderExcel(); + cn.hutool.poi.excel.OrderExcel order2 = new cn.hutool.poi.excel.OrderExcel(); order1.setId("2"); order1.setNum("456"); order1.setBody("body2"); - List rows = CollUtil.newArrayList(order1, order2); + List rows = CollUtil.newArrayList(order1, order2); // 通过工具类创建writer String file = "f:/test/writeBeanTest2.xlsx"; FileUtil.del(file); diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/OrderExcel.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/OrderExcel.java index 279221787..10bf20af9 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/OrderExcel.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/OrderExcel.java @@ -1,4 +1,4 @@ -package cn.hutool.poi.excel.test; +package cn.hutool.poi.excel; import lombok.Data; diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/TestBean.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/TestBean.java index 62c8405ba..a1ee1644a 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/TestBean.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/TestBean.java @@ -1,4 +1,4 @@ -package cn.hutool.poi.excel.test; +package cn.hutool.poi.excel; import lombok.Data; diff --git a/hutool-poi/src/test/java/cn/hutool/poi/word/WordWriterTest.java b/hutool-poi/src/test/java/cn/hutool/poi/word/WordWriterTest.java index b05131edd..22effb9a8 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/word/WordWriterTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/word/WordWriterTest.java @@ -1,4 +1,4 @@ -package cn.hutool.poi.word.test; +package cn.hutool.poi.word; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.io.FileUtil; diff --git a/hutool-poi/src/test/resources/data_for_sax_test.xls b/hutool-poi/src/test/resources/data_for_sax_test.xls index ebebc51bc0b1676a907911e2862d27cc5934857c..a5f18d6340d40a3e94a6954e02be71c8bcf4ea50 100644 GIT binary patch delta 3239 zcmbVOUuaWT82`@AO?p$>G_AHbN!wV{vQgLCrq&9zX{z9ZqYsWJ}6iODT;~KhY99E<~D*<6n&V=#-T0|={f9AASXA! z^Sj^g`+et}@7~OBDDww8)r*Es$D8%F0C4@&Yba-l)fPdFN;xmck7} zwXIW#{J=4XS($Bv{RAI?^6XVlyT#nABSdCzYsGsjF`t482cqakWBCB}QXGLH#J6bD zRG?p&aB;sB*T9qt08}gRemS0kA?XByq*NGSSjq?Rm+{=7H=}u@`bxNgeZC&g0Nlir zzBSeXn1en*yPyqX@WPJ43UbvBUF6#iy+HA`&RCh<4&DF2h!IANxp&05j_MO|3kEqc zi&I-k6#tE;npad#l0sE7FRK4Q)|7tU+d=2IIOOK5e)Y!aH3EVgr711nR^%@$;yV7O zw#By|@*BoY{HnIYaMQ%`pozN!4Lq<@1g1qGF8~oQQP!ZABb9Ud7@(dK(proX3w4<1j(>qZ!bc{o LnpIS#^j`NjY>e)( delta 2883 zcmbVOT}V_x6h3oz)w^o@r|sQ8*G;X^3R!8U3aiXqI+_mVT=%t^R?T?^9>Y>1g2nwVJ3Gz0x+ufa+oq6dluIxA8eCK>;<{WNn z0i}MS3u{rw#aPJR4gk}=_ff_~5roEZ#AdMHfpHD>9DjN;2DLClsAlp4BLCn_j#*rd z!VtkXKwUhgssa6%bq_8jxTK|5i03`V;LqtPh7T48Q6)bDNilwuxTPl`=W&Mz|vfDV1ZMD+8vfDV1BPtf>@rjDXdDNCF zB61$Ls#u)Ib8=Q|8|U$zip6>K`J{QZws9T@R4mTpq>9CPoKvwlk1dAK#D@*$vX2_g z{2kT*-FdzOhTtvFGUFV}=a;gCY7Y3Bd}uRG_$AFmN4)v+wOMGUccM6LIm&wRlVSM+ zP3yVFtdQ7YEs{RXJ@`m~NsG?9bbu*SHqgAA)3u_{&(9Zfq_Jp5$z;;BbZIORb1)d8 zm=6D_9 z8d>!*zh5nC^n{z_MrBoGO<5VSm}vBQSW!hYd!>-V{~u8Z57#yDt&$)509+G1suP;P zVsXRbkBY?&i_H;*7B?(LRV;2;WK}F~Sa|9cCuEx0k|L(l>5{9+V%7;!j9$g_^}<#B zR?l2T+H=8Z5y%{fUUO?7+OZy+)IP7t+>X7kuB4@o^a9lE$c%Qa_IVndu3US8<|HfR zDDbnLdvL7(MB_)OhF1jXgfKYCr%Us~Lt0(vreE~IU;4vN8+q@c?>;v2K`2wTYZ8s^ ze(Qkcp_}oCACN8M*|O4=9=hSy7fsV#MYr7<@#lWD(I43nzk{y)A%Bb0y)|eoaY#?y hUGbog?8ZkEr?s;?@b5$dwcz37$?8_JhrVag=Pwy3crgF~ diff --git a/hutool-poi/src/test/resources/data_for_sax_test.xlsx b/hutool-poi/src/test/resources/data_for_sax_test.xlsx index 8fb64fd8b5f1031ac5f20b8f28ac3d6994cd6e07..ecbac56a91bb6f493728d905ac3d37c2e603c416 100644 GIT binary patch delta 2823 zcmY*bX*kpm7afLS#x_ZeC5@QT46Fxc0dH?VIbe`wl=X|;MoO{yEI?P{EAutiw5|KK35J&+++=B1`6#qOa zcw}M2AG(8r7;i0l?6ZgNz^@l`3*r1b(~}u1 ze6ijRFUK2@Nl=%l_Ti8+m4=vh#%ko(+G;#D8HS|d3H5#&8J44Wo5ah`2hchD6DTtm z_T#pC)xU*20qgJ^fz~kclPG#FTJ)UbkgkBl}oE4AHKsGA@$*Y?H=FqF_p=jBxaDznJk+AQ-W(`N=O&{Ri**=?Poj|@fXuzMT zb8y?I4Uqd(QeL?Fb^4H#SpDO=Jr4$W*6nh_?4e6=xLw{-zfu9*B?QIi0WQ-8ScJ=D z*a8UN-0yvaZO1N8PKv;~px)WOx6BO&rCF`B9wz5359l>iCm^oMG9Anr`LtHZ9U<^t zgU1$y1hSXLpu5CEQ>@G6)@tY&wu>q^7^dyoGuG3g5JT~joLB%#Ck<{svMytZGWnf< z&Kxi*zxN}0`|gRpvzJMVt)U~(0mg=2HwsjJSq%2wHko#dI`}NpXTi;T!b?*V-7=3j zEtq>eEty{tcMo=WO-Q((wQyq9CB^Vb@W}zTd-h~ZKXu4oSEA)~k*4&|-F&g2O>oT; zj#+1p@$UH*v+DO{l{Y|XolBIk2WC;Mu#nKTO>(ueHXvUKN7+x&^hf}O(XM(x}TlkT5vy*jTZxIi2uCTingfSsE9T{V=na)^kx+wChThPz?Ji zh6acxT*+><_cvFYlK^0;9Lfi+(ihf0n=*cZ?>LtG57SVuhvL_q#kWk!oLw3@llC@lm`qE(?T;AZUIxpu%-C5a*99zN^vV@(AT)F-n2qBQ7-ldN+_h1I zRjg!0pjnG#$}DaIwJV=s&65VQ5`&~8x@D(ucai-zl|e4PmRlRlKGF3b?QcUr>x!ebvHzF?Mm_F=hNGInr(K z_vPo}&mIXpjGfnrr`DAH)}#i*KG3`)kP6JGjy&rFE9AN zeRFu6c9R^x24Ps2PwknS>Z^6cAnuoxtcK@@<_1MSfqrXkTd1HLPWeT2ZH^bGi{q;T z+}XjCdz-;>_mEGs1u||f=;LbSX5y}@PA}eJuXig8gm$-@w_H~9?B_HS1OK?u^l7^0 zs@zfaBWdK8IEMF{m|@G%NTfa`ngHuRAW3X(K0p1viO&F34GUw&SvIjmZ#yJ>fn3v4 zv^Um7mB#sW!SW{Q<0svVNl~4ng}>AQf`ImwLK>oxEGnhgxhb~l`t+}9unzeudWhoWNZky)l%?6!vK7_u=1ct*-etj_2*!B z!|`o<^tUz}b~`^_1rz17Ge%0^L4$Rdr}UhRC#F3sp6AB$D9${@xeW|0ILmYcy(cVo zb)@_3&)c0j`UBw@wcWxwPIl2LCcmLykemAZ#Uu~R$-2iaIr~qm847INmj0^6wF=2R z#*%`Ll34@c+&Y~*BeA`kj;aw0_H2!a)GuraOcr~cyF#A?{V;;80>`wU^t0NudN0oK zGYQd;(_%*X%|A@$nd#L&?saaE%unG_xFyKZ(wi239<0u~;CiF4H=v8*vk5Jrab$E9 zW*D=i&akv8yL9dkvpWF(M^6#F9~l_4!#zEW>#qr-=>P}gE-6IZCgUINzKFbrL#OyA zaArt-^rFs>iz1s%E_wGvW_KugP4*-+_-$f7QS{3j5eLENGW!g!EGStneFjG4E6%ui z&$W#YRb*{)o{wMo- zcY(~A03hSk6n^Z(SFd*SBZwd0kx+I^Ml?;Cl4>ntG6RmsV5#a@0>NLO+{S zg-JWE$~T6|8kUX`gl@o%Jr;UK!fc{i{31gnb%RDWe6|VjXb!zYKF`nYx9_}lc&aJ{ zg}S~ix4*4jA#F2)c=ue6D!eb R^ua>JHS~Fip2&ZD{{fpEDQ^G( delta 2651 zcmY*bc{tPy8=Yas60(g9BZRSUpQ0&^LN3F_7%`}rLU*!co3dsy%94`&kjq$V>`P)4 zCHqdul944T+$>{^ean3J-aqd5-S@BaJnwnlf8O()bDC_EY+lXt9C>JPPs4``1PX-$ zt2~nAtF{xmu)8_c9KtSI=@xK70XKsCsO~E+E$Eu0%m_dTboTqizA-u2o>CIRzKgwe zF~0F-P{cre=k|urRm+X8#}Y1{=`v7wKs!l$M6dO{pLV_YqcWO$Axbt_7M@JYo;SayZv4p8M_4h->8S zZ>jN|;nE$bMkG|RId)L~I&Jtt(!m>h=@$;0S!vb;oaQ=!x=q)4gAG(Peqv zBwBt@_^|F+Oe-3M0pW>iRtuA^c4U0+c$c8kg}$++Y(I0!e-25LcSAGTQ7_Q zrEHV^^<-iPnp)b_hO6(i8x#xOh-v+yQ{YFMx@?g&sz63_)W(clha0`ah!a7r)x`!i zl!|5Vy-YQiY5vqDhnRZE!%sJUd*fvttPl*1Q;<^5UAn~-knH6;K+d8bOX4`BBQ>5{zM~FwZ+Rj2DRhkpQ+6gIpqBSvJ*u@Z+I`hhI3gMrZTeLSG`j zM5jqiS(pzj)%f-3kMS}v3;bf$rNlq#)!;!d0PbK>criN|1)D8s&5px6gmwa{#@?I_o)^Xb@-W&s`&I6{JMajY}_e-R;?)Gbx2c%8Ri^M6uRAg>@S>}k^4y>r4p~2ksp2TwZ4x{T zT9TEl>3HI`+}N{XUK$xTA5e#k6dB?1y{(C#jL&rm)Cll`TmuF={__L8EIhq-3 zdVQT_(vCt1QddKZD2Ia|Ov4ZAh_zhxcO6?uLT+6;liDeF@}4o%GsG>IVP&Y6*Sz9Y zB+5d1bSG)~&T%7Yu|kA7(OuwcaHiI_&0hYA7{pE1h#agliQ027%)Ffb3(6+ygF&K-)2O zvPUmw5T%Bd)eN+VTo-DT8tMid{WmV$ZcX_$AVi9A(et^b@@cih747XCaQ&FBWPjw% zhuhW7wp9b?t)%IHY}~ObHU^!@g!w%#mTZ`YT~@aUD)v&pR)BL6;Wn*jXS&$q4DJA5 zBViDGHxcDZQ>VCZG9tzsrB|5}mLqMi$ok(uEE{S*x>yhRgUaN-!pJY`p{r=6CN@KukG+z`Rm z(sDbhSjS*Fdt&lu(`Bg2)P--Um(F)h+JMgxeDI!_b;U4w8O`VIsxZ8?R2Ni!(pgD# zq&#GQLdTEHp zQS~nM%J3AirkUP+#wL4)3goGG!ew8GRl>B8>SmVAjc%6(u^wNMN!gr}p8SQWQ9Jq* zK1@cDbR?q(-qoY2CiPhrQ~T++vB6+@DkU(|+iGop5mHzFOFX3ASGajTg=r#xmof9! zI8CJA0EgwT!umR0Gt(kiWQqAg1v?I@+fTpqLpIroc9-u$qQwsy$R zWy1Lf!#ZoP4e@)~Z@uEy?cGYxiz*j&_gmJ!E9@%Ymh9-!**A4fuf6jy$fYXlUS_@9 zrcgMxAliam$@wsfBcom$5xukZ{jHlNnWwDSwx@eL*FRayhSCXV)|K(6w&vM^bM3&= zoLA~Vv})~P=mzN^HkKAXpR3x^{bFYI{STwD>;vXt+PA@`LDntNT=cxrlY(Y^06b*Z zK_m=N`#qGt;IKRw052~9=B%lmiYsgtBZnxtm{$8wG3*w&uJIWl+A-S1T_XuYy~VnIjqI`7I^i z+k1Ke7Tj|zLd1M~FpXAuGo$GD&HqG1Qrv1X3md$gN^UvIV6%_dyamr!cb|5_`>y6{ zfYsZ@pChifmrd-2vMfbvm^!R{)0MY(AA^r7tTz9^elL6fVPz#jzXw3Yn|@=wkMsLM z{pi6SMRUGKN|I1Xg(4g=415|EU;-eK9wz6w*I;fDi3b8eB|nUQ|HQA{|KHNam8`)5 zfGSc8@$dBf)5rdq*ML9L4D1HHK`P7q&&xm{p??^EffWg02YH6ePYO7xe1R)U8n8kj zfedAJu4n{cEu#jo{({E=go*;<@4tdUpqqb&wdlW&`~o Date: Sun, 6 Dec 2020 07:41:54 +0800 Subject: [PATCH 13/47] add page of sql support --- CHANGELOG.md | 3 +- .../java/cn/hutool/core/net/URLEncoder.java | 18 +- .../main/java/cn/hutool/db/AbstractDb.java | 74 +++- .../main/java/cn/hutool/db/DaoTemplate.java | 2 +- .../main/java/cn/hutool/db/DialectRunner.java | 298 ++++++++++++++ .../src/main/java/cn/hutool/db/Page.java | 55 ++- .../src/main/java/cn/hutool/db/Session.java | 6 +- .../main/java/cn/hutool/db/SqlConnRunner.java | 380 ++++-------------- .../main/java/cn/hutool/db/StatementUtil.java | 35 +- .../java/cn/hutool/db/dialect/Dialect.java | 79 ++-- .../db/dialect/impl/AnsiSqlDialect.java | 52 +-- .../cn/hutool/db/dialect/impl/H2Dialect.java | 4 +- .../hutool/db/dialect/impl/MysqlDialect.java | 4 +- .../hutool/db/dialect/impl/OracleDialect.java | 7 +- .../db/dialect/impl/PostgresqlDialect.java | 4 +- .../db/dialect/impl/SqlServer2012Dialect.java | 4 +- .../db/dialect/impl/Sqlite3Dialect.java | 4 +- .../cn/hutool/db/handler/NumberHandler.java | 2 +- .../src/main/java/cn/hutool/db/sql/Query.java | 19 +- .../java/cn/hutool/db/sql/SqlBuilder.java | 58 ++- .../java/cn/hutool/db/sql/SqlExecutor.java | 16 + .../java/cn/hutool/db/sql/SqlFormatter.java | 19 +- .../src/test/java/cn/hutool/db/DbTest.java | 19 + 23 files changed, 711 insertions(+), 451 deletions(-) create mode 100644 hutool-db/src/main/java/cn/hutool/db/DialectRunner.java diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd19a094..0042e17dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,13 @@ ------------------------------------------------------------------------------------------------------------- -# 5.5.3 (2020-12-05) +# 5.5.3 (2020-12-06) ### 新特性 * 【core 】 IdcardUtil增加行政区划83(issue#1277@Github) * 【core 】 multipart中int改为long,解决大文件上传越界问题(issue#I27WZ3@Gitee) * 【core 】 ListUtil.page增加检查(pr#224@Gitee) +* 【db 】 Db增加使用sql的page方法(issue#247@Gitee) ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java b/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java index 2bfe00ee3..1da457b75 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java @@ -26,7 +26,7 @@ public class URLEncoder implements Serializable { // --------------------------------------------------------------------------------------------- Static method start /** - * 默认{@link URLEncoder}
+ * 默认URLEncoder
* 默认的编码器针对URI路径编码,定义如下: * *
@@ -38,7 +38,7 @@ public class URLEncoder implements Serializable {
 	public static final URLEncoder DEFAULT = createDefault();
 
 	/**
-	 * 用于查询语句的{@link URLEncoder}
+ * 用于查询语句的URLEncoder
* 编码器针对URI路径编码,定义如下: * *
@@ -53,7 +53,7 @@ public class URLEncoder implements Serializable {
 	public static final URLEncoder QUERY = createQuery();
 
 	/**
-	 * 全编码的{@link URLEncoder}
+ * 全编码的URLEncoder
*
 	 * 	 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is
 	 * 	 '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' 不编码
@@ -63,7 +63,7 @@ public class URLEncoder implements Serializable {
 	public static final URLEncoder ALL = createAll();
 
 	/**
-	 * 创建默认{@link URLEncoder}
+ * 创建默认URLEncoder
* 默认的编码器针对URI路径编码,定义如下: * *
@@ -72,7 +72,7 @@ public class URLEncoder implements Serializable {
 	 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
 	 * 
* - * @return {@link URLEncoder} + * @return URLEncoder */ public static URLEncoder createDefault() { final URLEncoder encoder = new URLEncoder(); @@ -102,7 +102,7 @@ public class URLEncoder implements Serializable { } /** - * 创建用于查询语句的{@link URLEncoder}
+ * 创建用于查询语句的URLEncoder
* 编码器针对URI路径编码,定义如下: * *
@@ -114,7 +114,7 @@ public class URLEncoder implements Serializable {
 	 * 

* 详细见:https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm * - * @return {@link URLEncoder} + * @return URLEncoder */ public static URLEncoder createQuery() { final URLEncoder encoder = new URLEncoder(); @@ -133,7 +133,7 @@ public class URLEncoder implements Serializable { } /** - * 创建{@link URLEncoder}
+ * 创建URLEncoder
* 编码器针对URI路径编码,定义如下: * *

@@ -144,7 +144,7 @@ public class URLEncoder implements Serializable {
 	 * 

* 详细见:https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm * - * @return {@link URLEncoder} + * @return URLEncoder */ public static URLEncoder createAll() { final URLEncoder encoder = new URLEncoder(); diff --git a/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java b/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java index 86a05ba3b..21eaad316 100644 --- a/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java +++ b/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java @@ -12,6 +12,7 @@ import cn.hutool.db.sql.Condition; import cn.hutool.db.sql.Condition.LikeType; import cn.hutool.db.sql.LogicalOperator; import cn.hutool.db.sql.Query; +import cn.hutool.db.sql.SqlBuilder; import cn.hutool.db.sql.SqlExecutor; import cn.hutool.db.sql.SqlUtil; import cn.hutool.db.sql.Wrapper; @@ -660,7 +661,7 @@ public abstract class AbstractDb implements Serializable { * @return 复合条件的结果数 * @throws SQLException SQL执行异常 */ - public int count(Entity where) throws SQLException { + public long count(Entity where) throws SQLException { Connection conn = null; try { conn = this.getConnection(); @@ -670,6 +671,23 @@ public abstract class AbstractDb implements Serializable { } } + /** + * 结果的条目数 + * + * @param selectSql 查询SQL语句 + * @return 复合条件的结果数 + * @throws SQLException SQL执行异常 + */ + public long count(CharSequence selectSql) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.count(conn, selectSql); + } finally { + this.closeConnection(conn); + } + } + /** * 分页查询
* 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 @@ -777,25 +795,59 @@ public abstract class AbstractDb implements Serializable { } } + /** + * 分页查询
+ * + * @param 结果对象类型 + * @param sql SQL构建器,可以使用{@link SqlBuilder#of(CharSequence)} 包装普通SQL + * @param page 分页对象 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 5.5.3 + */ + public T page(CharSequence sql, Page page, RsHandler rsh) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.page(conn, SqlBuilder.of(sql), page, rsh); + } finally { + this.closeConnection(conn); + } + } + + /** + * 分页查询 + * + * @param sql SQL语句字符串 + * @param page 分页对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 5.5.3 + */ + public PageResult page(CharSequence sql, Page page) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.page(conn, SqlBuilder.of(sql), page); + } finally { + this.closeConnection(conn); + } + } + /** * 分页查询
* 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 * * @param fields 返回的字段列表,null则返回所有字段 * @param where 条件实体类(包含表名) - * @param page 分页对象 - * @param numPerPage 每页条目数 + * @param pageNumber 页码 + * @param pageSize 每页结果数 * @return 结果对象 * @throws SQLException SQL执行异常 */ - public PageResult page(Collection fields, Entity where, int page, int numPerPage) throws SQLException { - Connection conn = null; - try { - conn = this.getConnection(); - return runner.page(conn, fields, where, page, numPerPage); - } finally { - this.closeConnection(conn); - } + public PageResult page(Collection fields, Entity where, int pageNumber, int pageSize) throws SQLException { + return page(fields, where, new Page(pageNumber, pageSize)); } /** diff --git a/hutool-db/src/main/java/cn/hutool/db/DaoTemplate.java b/hutool-db/src/main/java/cn/hutool/db/DaoTemplate.java index 2ba3e908b..ffe6dedd1 100644 --- a/hutool-db/src/main/java/cn/hutool/db/DaoTemplate.java +++ b/hutool-db/src/main/java/cn/hutool/db/DaoTemplate.java @@ -326,7 +326,7 @@ public class DaoTemplate { * @return 数量 * @throws SQLException SQL执行异常 */ - public int count(Entity where) throws SQLException{ + public long count(Entity where) throws SQLException{ return db.count(fixEntity(where)); } diff --git a/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java b/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java new file mode 100644 index 000000000..799ce1d31 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java @@ -0,0 +1,298 @@ +package cn.hutool.db; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.dialect.Dialect; +import cn.hutool.db.dialect.DialectFactory; +import cn.hutool.db.handler.NumberHandler; +import cn.hutool.db.handler.RsHandler; +import cn.hutool.db.sql.Query; +import cn.hutool.db.sql.SqlBuilder; +import cn.hutool.db.sql.SqlExecutor; +import cn.hutool.db.sql.SqlUtil; +import cn.hutool.db.sql.Wrapper; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class DialectRunner implements Serializable { + private static final long serialVersionUID = 1L; + + private Dialect dialect; + /** + * 是否大小写不敏感(默认大小写不敏感) + */ + protected boolean caseInsensitive = GlobalDbConfig.caseInsensitive; + + /** + * 构造 + * + * @param dialect 方言 + */ + public DialectRunner(Dialect dialect) { + this.dialect = dialect; + } + + /** + * 构造 + * + * @param driverClassName 驱动类名,用于识别方言 + */ + public DialectRunner(String driverClassName) { + this(DialectFactory.newDialect(driverClassName)); + } + + //---------------------------------------------------------------------------- CRUD start + /** + * 批量插入数据
+ * 批量插入必须严格保持Entity的结构一致,不一致会导致插入数据出现不可预知的结果
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接 + * @param records 记录列表,记录KV必须严格一致 + * @return 插入行数 + * @throws SQLException SQL执行异常 + */ + public int[] insert(Connection conn, Entity... records) throws SQLException { + checkConn(conn); + if (ArrayUtil.isEmpty(records)) { + return new int[]{0}; + } + + PreparedStatement ps = null; + try { + if(1 == records.length){ + //单条单独处理 + ps = dialect.psForInsert(conn, records[0]); + return new int[]{ps.executeUpdate()}; + } + + // 批量 + ps = dialect.psForInsertBatch(conn, records); + return ps.executeBatch(); + } finally { + DbUtil.close(ps); + } + } + + /** + * 插入数据
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接 + * @param record 记录 + * @return 主键列表 + * @throws SQLException SQL执行异常 + */ + public T insert(Connection conn, Entity record, RsHandler generatedKeysHandler) throws SQLException { + checkConn(conn); + if (CollUtil.isEmpty(record)) { + throw new SQLException("Empty entity provided!"); + } + + PreparedStatement ps = null; + try { + ps = dialect.psForInsert(conn, record); + ps.executeUpdate(); + if(null == generatedKeysHandler){ + return null; + } + return StatementUtil.getGeneratedKeys(ps, generatedKeysHandler); + } finally { + DbUtil.close(ps); + } + } + + /** + * 删除数据
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接 + * @param where 条件 + * @return 影响行数 + * @throws SQLException SQL执行异常 + */ + public int del(Connection conn, Entity where) throws SQLException { + checkConn(conn); + if (CollUtil.isEmpty(where)) { + //不允许做全表删除 + throw new SQLException("Empty entity provided!"); + } + + PreparedStatement ps = null; + try { + ps = dialect.psForDelete(conn, Query.of(where)); + return ps.executeUpdate(); + } finally { + DbUtil.close(ps); + } + } + + /** + * 更新数据
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接 + * @param record 记录 + * @param where 条件 + * @return 影响行数 + * @throws SQLException SQL执行异常 + */ + public int update(Connection conn, Entity record, Entity where) throws SQLException { + checkConn(conn); + if (CollUtil.isEmpty(record)) { + throw new SQLException("Empty entity provided!"); + } + if (CollUtil.isEmpty(where)) { + //不允许做全表更新 + throw new SQLException("Empty where provided!"); + } + + //表名可以从被更新记录的Entity中获得,也可以从Where中获得 + String tableName = record.getTableName(); + if (StrUtil.isBlank(tableName)) { + tableName = where.getTableName(); + record.setTableName(tableName); + } + + final Query query = new Query(SqlUtil.buildConditions(where), tableName); + PreparedStatement ps = null; + try { + ps = dialect.psForUpdate(conn, record, query); + return ps.executeUpdate(); + } finally { + DbUtil.close(ps); + } + } + + /** + * 查询
+ * 此方法不会关闭Connection + * + * @param 结果对象类型 + * @param conn 数据库连接对象 + * @param query {@link Query} + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T find(Connection conn, Query query, RsHandler rsh) throws SQLException { + checkConn(conn); + Assert.notNull(query, "[query] is null !"); + return SqlExecutor.queryAndClosePs(dialect.psForFind(conn, query), rsh); + } + + /** + * 获取结果总数,生成类似于select count(1) from XXX wher XXX=? and YYY=? + * + * @param conn 数据库连接对象 + * @param where 查询条件 + * @return 复合条件的结果数 + * @throws SQLException SQL执行异常 + */ + public long count(Connection conn, Entity where) throws SQLException { + checkConn(conn); + return SqlExecutor.queryAndClosePs(dialect.psForCount(conn, Query.of(where)), new NumberHandler()).longValue(); + } + + /** + * 分页查询
+ * 此方法不会关闭Connection + * + * @param 结果对象类型 + * @param conn 数据库连接对象 + * @param query 查询条件(包含表名) + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T page(Connection conn, Query query, RsHandler rsh) throws SQLException { + checkConn(conn); + if (null == query.getPage()) { + return this.find(conn, query, rsh); + } + + return SqlExecutor.queryAndClosePs(dialect.psForPage(conn, query), rsh); + } + + /** + * 分页查询
+ * 此方法不会关闭Connection + * + * @param 结果对象类型 + * @param conn 数据库连接对象 + * @param sqlBuilder SQL构建器,可以使用{@link SqlBuilder#of(CharSequence)} 包装普通SQL + * @param page 分页对象 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 5.5.3 + */ + public T page(Connection conn, SqlBuilder sqlBuilder, Page page, RsHandler rsh) throws SQLException { + checkConn(conn); + if (null == page) { + return SqlExecutor.query(conn, sqlBuilder, rsh); + } + + return SqlExecutor.queryAndClosePs(dialect.psForPage(conn, sqlBuilder, page), rsh); + } + //---------------------------------------------------------------------------- CRUD end + + //---------------------------------------------------------------------------- Getters and Setters start + + /** + * 设置是否在结果中忽略大小写
+ * 如果忽略,则在Entity中调用getXXX时,字段值忽略大小写,默认忽略 + * + * @param caseInsensitive 否在结果中忽略大小写 + * @since 5.2.4 + */ + public void setCaseInsensitive(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + } + + /** + * @return SQL方言 + */ + public Dialect getDialect() { + return dialect; + } + + /** + * 设置SQL方言 + * + * @param dialect 方言 + */ + public void setDialect(Dialect dialect) { + this.dialect = dialect; + } + + /** + * 设置包装器,包装器用于对表名、字段名进行符号包装(例如双引号),防止关键字与这些表名或字段冲突 + * + * @param wrapperChar 包装字符,字符会在SQL生成时位于表名和字段名两边,null时表示取消包装 + */ + public void setWrapper(Character wrapperChar) { + setWrapper(new Wrapper(wrapperChar)); + } + + /** + * 设置包装器,包装器用于对表名、字段名进行符号包装(例如双引号),防止关键字与这些表名或字段冲突 + * + * @param wrapper 包装器,null表示取消包装 + */ + public void setWrapper(Wrapper wrapper) { + this.dialect.setWrapper(wrapper); + } + //---------------------------------------------------------------------------- Getters and Setters end + + //---------------------------------------------------------------------------- Private method start + private void checkConn(Connection conn) { + Assert.notNull(conn, "Connection object must be not null!"); + } + //---------------------------------------------------------------------------- Private method start +} diff --git a/hutool-db/src/main/java/cn/hutool/db/Page.java b/hutool-db/src/main/java/cn/hutool/db/Page.java index d53b6bc99..8841ee4a3 100644 --- a/hutool-db/src/main/java/cn/hutool/db/Page.java +++ b/hutool-db/src/main/java/cn/hutool/db/Page.java @@ -9,26 +9,44 @@ import java.util.Arrays; /** * 分页对象 - * - * @author Looly * + * @author Looly */ public class Page implements Serializable { private static final long serialVersionUID = 97792549823353462L; public static final int DEFAULT_PAGE_SIZE = 20; - /** 页码,0表示第一页 */ + /** + * 页码,0表示第一页 + */ private int pageNumber; - /** 每页结果数 */ + /** + * 每页结果数 + */ private int pageSize; - /** 排序 */ + /** + * 排序 + */ private Order[] orders; + /** + * 创建Page对象 + * + * @param pageNumber 页码,0表示第一页 + * @param pageSize 每页结果数 + * @return Page + * @since 5.5.3 + */ + public static Page of(int pageNumber, int pageSize) { + return new Page(pageNumber, pageSize); + } + // ---------------------------------------------------------- Constructor start + /** * 构造,默认第0页,每页{@value #DEFAULT_PAGE_SIZE} 条 - * + * * @since 4.5.16 */ public Page() { @@ -37,9 +55,9 @@ public class Page implements Serializable { /** * 构造 - * + * * @param pageNumber 页码,0表示第一页 - * @param pageSize 每页结果数 + * @param pageSize 每页结果数 */ public Page(int pageNumber, int pageSize) { this.pageNumber = Math.max(pageNumber, 0); @@ -48,18 +66,19 @@ public class Page implements Serializable { /** * 构造 - * + * * @param pageNumber 页码,0表示第一页 - * @param pageSize 每页结果数 - * @param order 排序对象 + * @param pageSize 每页结果数 + * @param order 排序对象 */ public Page(int pageNumber, int pageSize, Order order) { this(pageNumber, pageSize); - this.orders = new Order[] { order }; + this.orders = new Order[]{order}; } // ---------------------------------------------------------- Constructor start // ---------------------------------------------------------- Getters and Setters start + /** * @return 页码,0表示第一页 */ @@ -69,7 +88,7 @@ public class Page implements Serializable { /** * 设置页码,0表示第一页 - * + * * @param pageNumber 页码 */ public void setPageNumber(int pageNumber) { @@ -87,7 +106,7 @@ public class Page implements Serializable { /** * 设置每页结果数 - * + * * @param pageSize 每页结果数 * @deprecated 使用 {@link #setPageSize(int)} 代替 */ @@ -105,7 +124,7 @@ public class Page implements Serializable { /** * 设置每页结果数 - * + * * @param pageSize 每页结果数 */ public void setPageSize(int pageSize) { @@ -121,7 +140,7 @@ public class Page implements Serializable { /** * 设置排序 - * + * * @param orders 排序 */ public void setOrder(Order... orders) { @@ -130,7 +149,7 @@ public class Page implements Serializable { /** * 设置排序 - * + * * @param orders 排序 */ public void addOrder(Order... orders) { @@ -162,7 +181,7 @@ public class Page implements Serializable { * 页码:2,每页10 =》 [21, 30] * 。。。 *

- * + * * @return 第一个数为开始位置,第二个数为结束位置 */ public int[] getStartEnd() { diff --git a/hutool-db/src/main/java/cn/hutool/db/Session.java b/hutool-db/src/main/java/cn/hutool/db/Session.java index 8b3a223e8..23cff582d 100644 --- a/hutool-db/src/main/java/cn/hutool/db/Session.java +++ b/hutool-db/src/main/java/cn/hutool/db/Session.java @@ -31,7 +31,7 @@ public class Session extends AbstractDb implements Closeable { /** * 创建默认数据源会话 * - * @return {@link Session} + * @return Session * @since 3.2.3 */ public static Session create() { @@ -42,7 +42,7 @@ public class Session extends AbstractDb implements Closeable { * 创建会话 * * @param group 分组 - * @return {@link Session} + * @return Session * @since 4.0.11 */ public static Session create(String group) { @@ -53,7 +53,7 @@ public class Session extends AbstractDb implements Closeable { * 创建会话 * * @param ds 数据源 - * @return {@link Session} + * @return Session */ public static Session create(DataSource ds) { return new Session(ds); diff --git a/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java b/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java index ac428c8b7..212a4090b 100644 --- a/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java +++ b/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java @@ -1,26 +1,21 @@ package cn.hutool.db; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.StrUtil; import cn.hutool.db.dialect.Dialect; import cn.hutool.db.dialect.DialectFactory; import cn.hutool.db.handler.EntityListHandler; +import cn.hutool.db.handler.HandleHelper; import cn.hutool.db.handler.NumberHandler; import cn.hutool.db.handler.PageResultHandler; import cn.hutool.db.handler.RsHandler; import cn.hutool.db.sql.Condition.LikeType; import cn.hutool.db.sql.Query; -import cn.hutool.db.sql.SqlExecutor; +import cn.hutool.db.sql.SqlBuilder; import cn.hutool.db.sql.SqlUtil; -import cn.hutool.db.sql.Wrapper; import javax.sql.DataSource; -import java.io.Serializable; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Collection; import java.util.List; @@ -32,15 +27,9 @@ import java.util.List; * * @author Luxiaolei */ -public class SqlConnRunner implements Serializable { +public class SqlConnRunner extends DialectRunner { private static final long serialVersionUID = 1L; - private Dialect dialect; - /** - * 是否大小写不敏感(默认大小写不敏感) - */ - protected boolean caseInsensitive = GlobalDbConfig.caseInsensitive; - /** * 实例化一个新的SQL运行对象 * @@ -79,7 +68,7 @@ public class SqlConnRunner implements Serializable { * @param dialect 方言 */ public SqlConnRunner(Dialect dialect) { - this.dialect = dialect; + super(dialect); } /** @@ -88,35 +77,12 @@ public class SqlConnRunner implements Serializable { * @param driverClassName 驱动类名,,用于识别方言 */ public SqlConnRunner(String driverClassName) { - this(DialectFactory.newDialect(driverClassName)); + super(driverClassName); } //------------------------------------------------------- Constructor end //---------------------------------------------------------------------------- CRUD start - /** - * 插入数据
- * 此方法不会关闭Connection - * - * @param conn 数据库连接 - * @param record 记录 - * @return 插入行数 - * @throws SQLException SQL执行异常 - */ - public int insert(Connection conn, Entity record) throws SQLException { - checkConn(conn); - if (CollUtil.isEmpty(record)) { - throw new SQLException("Empty entity provided!"); - } - PreparedStatement ps = null; - try { - ps = dialect.psForInsert(conn, record); - return ps.executeUpdate(); - } finally { - DbUtil.close(ps); - } - } - /** * 插入或更新数据
* 此方法不会关闭Connection @@ -152,33 +118,16 @@ public class SqlConnRunner implements Serializable { } /** - * 批量插入数据
- * 批量插入必须严格保持Entity的结构一致,不一致会导致插入数据出现不可预知的结果
+ * 插入数据
* 此方法不会关闭Connection * - * @param conn 数据库连接 - * @param records 记录列表,记录KV必须严格一致 + * @param conn 数据库连接 + * @param record 记录 * @return 插入行数 * @throws SQLException SQL执行异常 */ - public int[] insert(Connection conn, Entity... records) throws SQLException { - checkConn(conn); - if (ArrayUtil.isEmpty(records)) { - return new int[]{0}; - } - - //单条单独处理 - if (1 == records.length) { - return new int[]{insert(conn, records[0])}; - } - - PreparedStatement ps = null; - try { - ps = dialect.psForInsertBatch(conn, records); - return ps.executeBatch(); - } finally { - DbUtil.close(ps); - } + public int insert(Connection conn, Entity record) throws SQLException { + return insert(conn, new Entity[]{record})[0]; } /** @@ -191,19 +140,7 @@ public class SqlConnRunner implements Serializable { * @throws SQLException SQL执行异常 */ public List insertForGeneratedKeys(Connection conn, Entity record) throws SQLException { - checkConn(conn); - if (CollUtil.isEmpty(record)) { - throw new SQLException("Empty entity provided!"); - } - - PreparedStatement ps = null; - try { - ps = dialect.psForInsert(conn, record); - ps.executeUpdate(); - return StatementUtil.getGeneratedKeys(ps); - } finally { - DbUtil.close(ps); - } + return insert(conn, record, HandleHelper::handleRowToList); } /** @@ -216,106 +153,17 @@ public class SqlConnRunner implements Serializable { * @throws SQLException SQL执行异常 */ public Long insertForGeneratedKey(Connection conn, Entity record) throws SQLException { - checkConn(conn); - if (CollUtil.isEmpty(record)) { - throw new SQLException("Empty entity provided!"); - } - - PreparedStatement ps = null; - try { - ps = dialect.psForInsert(conn, record); - ps.executeUpdate(); - return StatementUtil.getGeneratedKeyOfLong(ps); - } finally { - DbUtil.close(ps); - } - } - - /** - * 删除数据
- * 此方法不会关闭Connection - * - * @param conn 数据库连接 - * @param where 条件 - * @return 影响行数 - * @throws SQLException SQL执行异常 - */ - public int del(Connection conn, Entity where) throws SQLException { - checkConn(conn); - if (CollUtil.isEmpty(where)) { - //不允许做全表删除 - throw new SQLException("Empty entity provided!"); - } - - final Query query = new Query(SqlUtil.buildConditions(where), where.getTableName()); - PreparedStatement ps = null; - try { - ps = dialect.psForDelete(conn, query); - return ps.executeUpdate(); - } finally { - DbUtil.close(ps); - } - } - - /** - * 更新数据
- * 此方法不会关闭Connection - * - * @param conn 数据库连接 - * @param record 记录 - * @param where 条件 - * @return 影响行数 - * @throws SQLException SQL执行异常 - */ - public int update(Connection conn, Entity record, Entity where) throws SQLException { - checkConn(conn); - if (CollUtil.isEmpty(record)) { - throw new SQLException("Empty entity provided!"); - } - if (CollUtil.isEmpty(where)) { - //不允许做全表更新 - throw new SQLException("Empty where provided!"); - } - - //表名可以从被更新记录的Entity中获得,也可以从Where中获得 - String tableName = record.getTableName(); - if (StrUtil.isBlank(tableName)) { - tableName = where.getTableName(); - record.setTableName(tableName); - } - - final Query query = new Query(SqlUtil.buildConditions(where), tableName); - PreparedStatement ps = null; - try { - ps = dialect.psForUpdate(conn, record, query); - return ps.executeUpdate(); - } finally { - DbUtil.close(ps); - } - } - - /** - * 查询
- * 此方法不会关闭Connection - * - * @param 结果对象类型 - * @param conn 数据库连接对象 - * @param query {@link Query} - * @param rsh 结果集处理对象 - * @return 结果对象 - * @throws SQLException SQL执行异常 - */ - public T find(Connection conn, Query query, RsHandler rsh) throws SQLException { - checkConn(conn); - Assert.notNull(query, "[query] is null !"); - - PreparedStatement ps = null; - try { - ps = dialect.psForFind(conn, query); - return SqlExecutor.query(ps, rsh); - } finally { - DbUtil.close(ps); - } + return insert(conn, record, (rs)->{ + Long generatedKey = null; + if (rs != null && rs.next()) { + try { + generatedKey = rs.getLong(1); + } catch (SQLException e) { + // 自增主键不为数字或者为Oracle的rowid,跳过 + } + } + return generatedKey; + }); } /** @@ -331,9 +179,7 @@ public class SqlConnRunner implements Serializable { * @throws SQLException SQL执行异常 */ public T find(Connection conn, Collection fields, Entity where, RsHandler rsh) throws SQLException { - final Query query = new Query(SqlUtil.buildConditions(where), where.getTableName()); - query.setFields(fields); - return find(conn, query, rsh); + return find(conn, Query.of(where).setFields(fields), rsh); } /** @@ -433,24 +279,16 @@ public class SqlConnRunner implements Serializable { } /** - * 结果的条目数 + * 获取查询结果总数,生成类似于 SELECT count(1) from (sql) * - * @param conn 数据库连接对象 - * @param where 查询条件 - * @return 复合条件的结果数 - * @throws SQLException SQL执行异常 + * @param conn 数据库连接对象 + * @param selectSql 查询语句 + * @return 结果数 + * @throws SQLException SQL异常 */ - public int count(Connection conn, Entity where) throws SQLException { - checkConn(conn); - - final Query query = new Query(SqlUtil.buildConditions(where), where.getTableName()); - PreparedStatement ps = null; - try { - ps = dialect.psForCount(conn, query); - return SqlExecutor.query(ps, new NumberHandler()).intValue(); - } finally { - DbUtil.close(ps); - } + public long count(Connection conn, CharSequence selectSql) throws SQLException { + SqlBuilder sqlBuilder = SqlBuilder.of(selectSql).insertPreFragment("SELECT count(1) from(").append(")"); + return page(conn, sqlBuilder, null, new NumberHandler()).intValue(); } /** @@ -468,32 +306,25 @@ public class SqlConnRunner implements Serializable { * @throws SQLException SQL执行异常 */ public T page(Connection conn, Collection fields, Entity where, int pageNumber, int numPerPage, RsHandler rsh) throws SQLException { - return page(conn, fields, where, new Page(pageNumber, numPerPage), rsh); + return page(conn, Query.of(where).setFields(fields).setPage(new Page(pageNumber, numPerPage)), rsh); } /** * 分页查询
* 此方法不会关闭Connection * - * @param 结果对象类型 - * @param conn 数据库连接对象 - * @param fields 返回的字段列表,null则返回所有字段 - * @param where 条件实体类(包含表名) - * @param page 分页对象 - * @param rsh 结果集处理对象 + * @param conn 数据库连接对象 + * @param sqlBuilder SQL构建器,可以使用{@link SqlBuilder#of(CharSequence)} 包装普通SQL + * @param page 分页对象 * @return 结果对象 * @throws SQLException SQL执行异常 + * @since 5.5.3 */ - public T page(Connection conn, Collection fields, Entity where, Page page, RsHandler rsh) throws SQLException { - checkConn(conn); - if (null == page) { - return this.find(conn, fields, where, rsh); - } - - final Query query = new Query(SqlUtil.buildConditions(where), where.getTableName()); - query.setFields(fields); - query.setPage(page); - return SqlExecutor.queryAndClosePs(dialect.psForPage(conn, query), rsh); + public PageResult page(Connection conn, SqlBuilder sqlBuilder, Page page) throws SQLException { + final PageResultHandler pageResultHandler = new PageResultHandler( + new PageResult<>(page.getPageNumber(), page.getPageSize(), (int)count(conn, sqlBuilder.build())), + this.caseInsensitive); + return page(conn, sqlBuilder, page, pageResultHandler); } /** @@ -509,38 +340,7 @@ public class SqlConnRunner implements Serializable { * @throws SQLException SQL执行异常 */ public PageResult page(Connection conn, Collection fields, Entity where, int page, int numPerPage) throws SQLException { - checkConn(conn); - - final int count = count(conn, where); - final PageResultHandler pageResultHandler = new PageResultHandler(new PageResult<>(page, numPerPage, count), this.caseInsensitive); - return this.page(conn, fields, where, page, numPerPage, pageResultHandler); - } - - /** - * 分页查询
- * 此方法不会关闭Connection - * - * @param conn 数据库连接对象 - * @param fields 返回的字段列表,null则返回所有字段 - * @param where 条件实体类(包含表名) - * @param page 分页对象 - * @return 结果对象 - * @throws SQLException SQL执行异常 - */ - public PageResult page(Connection conn, Collection fields, Entity where, Page page) throws SQLException { - checkConn(conn); - - //查询全部 - if (null == page) { - List entityList = this.find(conn, fields, where, new EntityListHandler(GlobalDbConfig.caseInsensitive)); - final PageResult pageResult = new PageResult<>(0, entityList.size(), entityList.size()); - pageResult.addAll(entityList); - return pageResult; - } - - final int count = count(conn, where); - PageResultHandler pageResultHandler = new PageResultHandler(new PageResult<>(page.getPageNumber(), page.getPageSize(), count), this.caseInsensitive); - return this.page(conn, fields, where, page, pageResultHandler); + return page(conn, fields, where, new Page(page, numPerPage)); } /** @@ -556,68 +356,38 @@ public class SqlConnRunner implements Serializable { public PageResult page(Connection conn, Entity where, Page page) throws SQLException { return this.page(conn, null, where, page); } + + /** + * 分页查询
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public PageResult page(Connection conn, Collection fields, Entity where, Page page) throws SQLException { + final PageResultHandler pageResultHandler = new PageResultHandler( + new PageResult<>(page.getPageNumber(), page.getPageSize(), (int)count(conn, where)), + this.caseInsensitive); + return page(conn, fields, where, page, pageResultHandler); + } + + /** + * 分页查询
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T page(Connection conn, Collection fields, Entity where, Page page, RsHandler handler) throws SQLException { + return this.page(conn, Query.of(where).setFields(fields).setPage(page), handler); + } //---------------------------------------------------------------------------- CRUD end - - //---------------------------------------------------------------------------- Getters and Setters end - - /** - * 设置是否在结果中忽略大小写
- * 如果忽略,则在Entity中调用getXXX时,字段值忽略大小写,默认忽略 - * - * @param caseInsensitive 否在结果中忽略大小写 - * @since 5.2.4 - */ - public void setCaseInsensitive(boolean caseInsensitive) { - this.caseInsensitive = caseInsensitive; - } - - /** - * @return SQL方言 - */ - public Dialect getDialect() { - return dialect; - } - - /** - * 设置SQL方言 - * - * @param dialect 方言 - * @return this - */ - public SqlConnRunner setDialect(Dialect dialect) { - this.dialect = dialect; - return this; - } - - /** - * 设置包装器,包装器用于对表名、字段名进行符号包装(例如双引号),防止关键字与这些表名或字段冲突 - * - * @param wrapperChar 包装字符,字符会在SQL生成时位于表名和字段名两边,null时表示取消包装 - * @return this - * @since 4.0.0 - */ - public SqlConnRunner setWrapper(Character wrapperChar) { - return setWrapper(new Wrapper(wrapperChar)); - } - - /** - * 设置包装器,包装器用于对表名、字段名进行符号包装(例如双引号),防止关键字与这些表名或字段冲突 - * - * @param wrapper 包装器,null表示取消包装 - * @return this - * @since 4.0.0 - */ - public SqlConnRunner setWrapper(Wrapper wrapper) { - this.dialect.setWrapper(wrapper); - return this; - } - //---------------------------------------------------------------------------- Getters and Setters end - - //---------------------------------------------------------------------------- Private method start - private void checkConn(Connection conn) { - if (null == conn) { - throw new NullPointerException("Connection object is null!"); - } - } - //---------------------------------------------------------------------------- Private method start } \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java b/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java index 413e5a5ce..67a07f089 100644 --- a/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java +++ b/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java @@ -6,6 +6,8 @@ import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.db.handler.HandleHelper; +import cn.hutool.db.handler.RsHandler; import cn.hutool.db.sql.NamedSql; import cn.hutool.db.sql.SqlBuilder; import cn.hutool.db.sql.SqlLog; @@ -21,7 +23,6 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -230,14 +231,14 @@ public class StatementUtil { /** * 获得自增键的值
- * 此方法对于Oracle无效 + * 此方法对于Oracle无效(返回null) * * @param ps PreparedStatement - * @return 自增键的值 + * @return 自增键的值,不存在返回null * @throws SQLException SQL执行异常 */ public static Long getGeneratedKeyOfLong(Statement ps) throws SQLException { - try (final ResultSet rs = ps.getGeneratedKeys()) { + return getGeneratedKeys(ps, (rs)->{ Long generatedKey = null; if (rs != null && rs.next()) { try { @@ -247,7 +248,7 @@ public class StatementUtil { } } return generatedKey; - } + }); } /** @@ -258,15 +259,21 @@ public class StatementUtil { * @throws SQLException SQL执行异常 */ public static List getGeneratedKeys(Statement ps) throws SQLException { - final List keys = new ArrayList<>(); - try (final ResultSet rs = ps.getGeneratedKeys()) { - if (null != rs) { - int i = 1; - while (rs.next()) { - keys.add(rs.getObject(i++)); - } - } - return keys; + return getGeneratedKeys(ps, HandleHelper::handleRowToList); + } + + /** + * 获取主键,并使用{@link RsHandler} 处理后返回 + * @param statement {@link Statement} + * @param rsHandler 主键结果集处理器 + * @param 自定义主键类型 + * @return 主键 + * @throws SQLException SQL执行异常 + * @since 5.5.3 + */ + public static T getGeneratedKeys(Statement statement, RsHandler rsHandler) throws SQLException { + try (final ResultSet rs = statement.getGeneratedKeys()) { + return rsHandler.handle(rs); } } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java index 6bca51289..08677c1e8 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java @@ -1,23 +1,26 @@ package cn.hutool.db.dialect; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.db.Entity; +import cn.hutool.db.Page; +import cn.hutool.db.sql.Order; +import cn.hutool.db.sql.Query; +import cn.hutool.db.sql.SqlBuilder; +import cn.hutool.db.sql.Wrapper; + import java.io.Serializable; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import cn.hutool.db.Entity; -import cn.hutool.db.sql.Query; -import cn.hutool.db.sql.Wrapper; - /** * SQL方言,不同的数据库由于在某些SQL上有所区别,故为每种数据库配置不同的方言。
* 由于不同数据库间SQL语句的差异,导致无法统一拼接SQL,
* Dialect接口旨在根据不同的数据库,使用不同的方言实现类,来拼接对应的SQL,并将SQL和参数放入PreparedStatement中 - * - * @author loolly * + * @author loolly */ -public interface Dialect extends Serializable{ +public interface Dialect extends Serializable { /** * @return 包装器 @@ -26,36 +29,37 @@ public interface Dialect extends Serializable{ /** * 设置包装器 - * + * * @param wrapper 包装器 */ void setWrapper(Wrapper wrapper); // -------------------------------------------- Execute + /** * 构建用于插入的PreparedStatement - * - * @param conn 数据库连接对象 + * + * @param conn 数据库连接对象 * @param entity 数据实体类(包含表名) * @return PreparedStatement * @throws SQLException SQL执行异常 */ PreparedStatement psForInsert(Connection conn, Entity entity) throws SQLException; - + /** * 构建用于批量插入的PreparedStatement - * - * @param conn 数据库连接对象 + * + * @param conn 数据库连接对象 * @param entities 数据实体,实体的结构必须全部一致,否则插入结果将不可预知 * @return PreparedStatement * @throws SQLException SQL执行异常 */ PreparedStatement psForInsertBatch(Connection conn, Entity... entities) throws SQLException; - + /** * 构建用于删除的PreparedStatement - * - * @param conn 数据库连接对象 + * + * @param conn 数据库连接对象 * @param query 查找条件(包含表名) * @return PreparedStatement * @throws SQLException SQL执行异常 @@ -64,20 +68,21 @@ public interface Dialect extends Serializable{ /** * 构建用于更新的PreparedStatement - * - * @param conn 数据库连接对象 + * + * @param conn 数据库连接对象 * @param entity 数据实体类(包含表名) - * @param query 查找条件(包含表名) + * @param query 查找条件(包含表名) * @return PreparedStatement * @throws SQLException SQL执行异常 */ PreparedStatement psForUpdate(Connection conn, Entity entity, Query query) throws SQLException; // -------------------------------------------- Query + /** * 构建用于获取多条记录的PreparedStatement - * - * @param conn 数据库连接对象 + * + * @param conn 数据库连接对象 * @param query 查询条件(包含表名) * @return PreparedStatement * @throws SQLException SQL执行异常 @@ -86,28 +91,46 @@ public interface Dialect extends Serializable{ /** * 构建用于分页查询的PreparedStatement - * - * @param conn 数据库连接对象 + * + * @param conn 数据库连接对象 * @param query 查询条件(包含表名) * @return PreparedStatement * @throws SQLException SQL执行异常 */ PreparedStatement psForPage(Connection conn, Query query) throws SQLException; + /** + * 构建用于分页查询的PreparedStatement
+ * 可以在此方法中使用{@link SqlBuilder#orderBy(Order...)}方法加入排序信息, + * 排序信息通过{@link Page#getOrders()}获取 + * + * @param conn 数据库连接对象 + * @param sqlBuilder SQL构建器,可以使用{@link SqlBuilder#of(CharSequence)} 包装普通SQL + * @param page 分页对象 + * @return PreparedStatement + * @throws SQLException SQL执行异常 + * @since 5.5.3 + */ + PreparedStatement psForPage(Connection conn, SqlBuilder sqlBuilder, Page page) throws SQLException; + /** * 构建用于查询行数的PreparedStatement - * - * @param conn 数据库连接对象 + * + * @param conn 数据库连接对象 * @param query 查询条件(包含表名) * @return PreparedStatement * @throws SQLException SQL执行异常 */ - PreparedStatement psForCount(Connection conn, Query query) throws SQLException; + default PreparedStatement psForCount(Connection conn, Query query) throws SQLException{ + query.setFields(ListUtil.toList("count(1)")); + return psForFind(conn, query); + } /** * 方言名 - * + * * @return 方言名 + * @since 5.5.3 */ - DialectName dialectName(); + String dialectName(); } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java index 03b58dda1..3793d41f0 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java @@ -1,6 +1,5 @@ package cn.hutool.db.dialect.impl; -import cn.hutool.core.collection.ListUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; @@ -59,7 +58,7 @@ public class AnsiSqlDialect implements Dialect { @Override public PreparedStatement psForDelete(Connection conn, Query query) throws SQLException { - Assert.notNull(query, "query must not be null !"); + Assert.notNull(query, "query must be not null !"); final Condition[] where = query.getWhere(); if (ArrayUtil.isEmpty(where)) { @@ -73,7 +72,7 @@ public class AnsiSqlDialect implements Dialect { @Override public PreparedStatement psForUpdate(Connection conn, Entity entity, Query query) throws SQLException { - Assert.notNull(query, "query must not be null !"); + Assert.notNull(query, "query must be not null !"); final Condition[] where = query.getWhere(); if (ArrayUtil.isEmpty(where)) { @@ -88,32 +87,27 @@ public class AnsiSqlDialect implements Dialect { @Override public PreparedStatement psForFind(Connection conn, Query query) throws SQLException { - Assert.notNull(query, "query must not be null !"); - - final SqlBuilder find = SqlBuilder.create(wrapper).query(query); - - return StatementUtil.prepareStatement(conn, find); + return psForPage(conn, query); } @Override public PreparedStatement psForPage(Connection conn, Query query) throws SQLException { - // 验证 - if (query == null || StrUtil.hasBlank(query.getTableNames())) { - throw new DbRuntimeException("Table name must not be null !"); + Assert.notNull(query, "query must be not null !"); + if (StrUtil.hasBlank(query.getTableNames())) { + throw new DbRuntimeException("Table name must be not empty !"); } - final Page page = query.getPage(); - if (null == page) { - // 无分页信息默认使用find - return this.psForFind(conn, query); - } - - SqlBuilder find = SqlBuilder.create(wrapper).query(query).orderBy(page.getOrders()); + final SqlBuilder find = SqlBuilder.create(wrapper).query(query); + return psForPage(conn, find, query.getPage()); + } + @Override + public PreparedStatement psForPage(Connection conn, SqlBuilder sqlBuilder, Page page) throws SQLException { // 根据不同数据库在查询SQL语句基础上包装其分页的语句 - find = wrapPageSql(find, page); - - return StatementUtil.prepareStatement(conn, find); + if(null != page){ + sqlBuilder = wrapPageSql(sqlBuilder.orderBy(page.getOrders()), page); + } + return StatementUtil.prepareStatement(conn, sqlBuilder); } /** @@ -127,18 +121,16 @@ public class AnsiSqlDialect implements Dialect { */ protected SqlBuilder wrapPageSql(SqlBuilder find, Page page) { // limit A offset B 表示:A就是你需要多少行,B就是查询的起点位置。 - return find.append(" limit ").append(page.getPageSize()).append(" offset ").append(page.getStartPosition()); + return find + .append(" limit ") + .append(page.getPageSize()) + .append(" offset ") + .append(page.getStartPosition()); } @Override - public PreparedStatement psForCount(Connection conn, Query query) throws SQLException { - query.setFields(ListUtil.toList("count(1)")); - return psForFind(conn, query); - } - - @Override - public DialectName dialectName() { - return DialectName.ANSI; + public String dialectName() { + return DialectName.ANSI.name(); } // ---------------------------------------------------------------------------- Protected method start diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java index 404814c45..96311bb0f 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java @@ -17,8 +17,8 @@ public class H2Dialect extends AnsiSqlDialect { } @Override - public DialectName dialectName() { - return DialectName.H2; + public String dialectName() { + return DialectName.H2.name(); } @Override diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java index 528806ad8..d10e811fb 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java @@ -23,7 +23,7 @@ public class MysqlDialect extends AnsiSqlDialect{ } @Override - public DialectName dialectName() { - return DialectName.MYSQL; + public String dialectName() { + return DialectName.MYSQL.toString(); } } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java index d89c66e88..925889611 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java @@ -13,7 +13,8 @@ public class OracleDialect extends AnsiSqlDialect{ private static final long serialVersionUID = 6122761762247483015L; public OracleDialect() { -// wrapper = new Wrapper('"'); //Oracle所有字段名用双引号包围,防止字段名或表名与系统关键字冲突 + //Oracle所有字段名用双引号包围,防止字段名或表名与系统关键字冲突 + //wrapper = new Wrapper('"'); } @Override @@ -27,7 +28,7 @@ public class OracleDialect extends AnsiSqlDialect{ } @Override - public DialectName dialectName() { - return DialectName.ORACLE; + public String dialectName() { + return DialectName.ORACLE.name(); } } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java index 404130fea..d7109e3c2 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java @@ -17,7 +17,7 @@ public class PostgresqlDialect extends AnsiSqlDialect{ } @Override - public DialectName dialectName() { - return DialectName.POSTGREESQL; + public String dialectName() { + return DialectName.POSTGREESQL.name(); } } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/SqlServer2012Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/SqlServer2012Dialect.java index bc8e38588..5a4ab8a0d 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/SqlServer2012Dialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/SqlServer2012Dialect.java @@ -34,7 +34,7 @@ public class SqlServer2012Dialect extends AnsiSqlDialect { } @Override - public DialectName dialectName() { - return DialectName.SQLSERVER2012; + public String dialectName() { + return DialectName.SQLSERVER2012.name(); } } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/Sqlite3Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/Sqlite3Dialect.java index 1531b9fee..bf209a00c 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/Sqlite3Dialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/Sqlite3Dialect.java @@ -16,7 +16,7 @@ public class Sqlite3Dialect extends AnsiSqlDialect{ } @Override - public DialectName dialectName() { - return DialectName.SQLITE3; + public String dialectName() { + return DialectName.SQLITE3.name(); } } diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/NumberHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/NumberHandler.java index eb5b78a91..e8842fd6c 100644 --- a/hutool-db/src/main/java/cn/hutool/db/handler/NumberHandler.java +++ b/hutool-db/src/main/java/cn/hutool/db/handler/NumberHandler.java @@ -21,6 +21,6 @@ public class NumberHandler implements RsHandler{ @Override public Number handle(ResultSet rs) throws SQLException { - return rs.next() ? rs.getBigDecimal(1) : null; + return (null != rs && rs.next()) ? rs.getBigDecimal(1) : null; } } diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/Query.java b/hutool-db/src/main/java/cn/hutool/db/sql/Query.java index 12adbe889..0619e2097 100644 --- a/hutool-db/src/main/java/cn/hutool/db/sql/Query.java +++ b/hutool-db/src/main/java/cn/hutool/db/sql/Query.java @@ -1,12 +1,13 @@ package cn.hutool.db.sql; -import java.util.Collection; - import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.Entity; import cn.hutool.db.Page; +import java.util.Collection; + /** * 查询对象,用于传递查询所需的字段值
* 查询对象根据表名(可以多个),多个条件 {@link Condition} 构建查询对象完成查询。
@@ -26,6 +27,16 @@ public class Query { /** 分页对象 */ Page page; + /** + * 从{@link Entity}构建Query + * @param where 条件查询{@link Entity},包含条件Map和表名 + * @return Query + * @since 5.5.3 + */ + public static Query of(Entity where){ + return new Query(SqlUtil.buildConditions(where), where.getTableName()); + } + // --------------------------------------------------------------- Constructor start /** * 构造 @@ -147,9 +158,9 @@ public class Query { } /** - * 获得分页对象 + * 获得分页对象,无分页返回{@code null} * - * @return 分页对象 + * @return 分页对象 or {@code null} */ public Page getPage() { return page; diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java b/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java index 893a6dae9..e10e1a7a8 100644 --- a/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java +++ b/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java @@ -3,7 +3,6 @@ package cn.hutool.db.sql; import cn.hutool.core.builder.Builder; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.db.DbRuntimeException; import cn.hutool.db.Entity; @@ -12,6 +11,7 @@ import cn.hutool.db.dialect.DialectName; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map.Entry; @@ -46,6 +46,16 @@ public class SqlBuilder implements Builder{ return new SqlBuilder(wrapper); } + /** + * 从已有的SQL中构建一个SqlBuilder + * @param sql SQL语句 + * @return SqlBuilder + * @since 5.5.3 + */ + public static SqlBuilder of(CharSequence sql){ + return create().append(sql); + } + // --------------------------------------------------------------- Static methods end // --------------------------------------------------------------- Enums start @@ -99,12 +109,25 @@ public class SqlBuilder implements Builder{ /** * 插入
* 插入会忽略空的字段名及其对应值,但是对于有字段名对应值为{@code null}的情况不忽略 - * + * * @param entity 实体 * @param dialectName 方言名,用于对特殊数据库特殊处理 * @return 自己 */ public SqlBuilder insert(Entity entity, DialectName dialectName) { + return insert(entity, dialectName.name()); + } + + /** + * 插入
+ * 插入会忽略空的字段名及其对应值,但是对于有字段名对应值为{@code null}的情况不忽略 + * + * @param entity 实体 + * @param dialectName 方言名,用于对特殊数据库特殊处理 + * @return 自己 + * @since 5.5.3 + */ + public SqlBuilder insert(Entity entity, String dialectName) { // 验证 validateEntity(entity); @@ -114,7 +137,7 @@ public class SqlBuilder implements Builder{ entity.setTableName(wrapper.wrap(entity.getTableName())); } - final boolean isOracle = ObjectUtil.equal(dialectName, DialectName.ORACLE);// 对Oracle的特殊处理 + final boolean isOracle = StrUtil.equalsAnyIgnoreCase(dialectName, DialectName.ORACLE.name());// 对Oracle的特殊处理 final StringBuilder fieldsPart = new StringBuilder(); final StringBuilder placeHolder = new StringBuilder(); @@ -519,7 +542,14 @@ public class SqlBuilder implements Builder{ } /** - * 追加SQL其它部分片段 + * 追加SQL其它部分片段,此方法只是简单的追加SQL字符串,空格需手动加入,例如: + * + *
+	 *     SqlBuilder builder = SqlBuilder.of("select *");
+	 *     builder.append(" from ").append("user");
+	 * 
+ * + * 如果需要追加带占位符的片段,需调用{@link #addParams(Object...)} 方法加入对应参数值。 * * @param sqlFragment SQL其它部分片段 * @return this @@ -531,6 +561,26 @@ public class SqlBuilder implements Builder{ return this; } + /** + * 手动增加参数,调用此方法前需确认SQL中有对应占位符,主要用于自定义SQL片段中有占位符的情况,例如: + * + *
+	 *     SqlBuilder builder = SqlBuilder.of("select * from user where id=?");
+	 *     builder.append(" and name=?")
+	 *     builder.addParams(1, "looly");
+	 * 
+ * + * @param params 参数列表 + * @return this + * @since 5.5.3 + */ + public SqlBuilder addParams(Object... params){ + if(ArrayUtil.isNotEmpty(params)){ + Collections.addAll(this.paramValues, params); + } + return this; + } + /** * 构建查询SQL * diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/SqlExecutor.java b/hutool-db/src/main/java/cn/hutool/db/sql/SqlExecutor.java index 238c1b884..a64973625 100644 --- a/hutool-db/src/main/java/cn/hutool/db/sql/SqlExecutor.java +++ b/hutool-db/src/main/java/cn/hutool/db/sql/SqlExecutor.java @@ -259,6 +259,22 @@ public class SqlExecutor { } } + /** + * 执行查询语句
+ * 此方法不会关闭Connection + * + * @param 处理结果类型 + * @param conn 数据库连接对象 + * @param sqlBuilder SQL构建器,包含参数 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 5.5.3 + */ + public static T query(Connection conn, SqlBuilder sqlBuilder, RsHandler rsh) throws SQLException { + return query(conn, sqlBuilder.build(), rsh, sqlBuilder.getParamValueArray()); + } + // -------------------------------------------------------------------------------------- Execute With PreparedStatement /** * 用于执行 INSERT、UPDATE 或 DELETE 语句以及 SQL DDL(数据定义语言)语句,例如 CREATE TABLE 和 DROP TABLE。
diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/SqlFormatter.java b/hutool-db/src/main/java/cn/hutool/db/sql/SqlFormatter.java index 0ba99bde8..b8e4e46e5 100644 --- a/hutool-db/src/main/java/cn/hutool/db/sql/SqlFormatter.java +++ b/hutool-db/src/main/java/cn/hutool/db/sql/SqlFormatter.java @@ -4,6 +4,7 @@ import java.util.*; /** * SQL格式化器 from Hibernate + * * @author looly */ public class SqlFormatter { @@ -13,7 +14,7 @@ public class SqlFormatter { private static final Set QUANTIFIERS = new HashSet<>(); private static final Set DML = new HashSet<>(); private static final Set MISC = new HashSet<>(); - + static { BEGIN_CLAUSES.add("left"); BEGIN_CLAUSES.add("right"); @@ -21,7 +22,7 @@ public class SqlFormatter { BEGIN_CLAUSES.add("outer"); BEGIN_CLAUSES.add("group"); BEGIN_CLAUSES.add("order"); - + END_CLAUSES.add("where"); END_CLAUSES.add("set"); END_CLAUSES.add("having"); @@ -30,41 +31,41 @@ public class SqlFormatter { END_CLAUSES.add("by"); END_CLAUSES.add("into"); END_CLAUSES.add("union"); - + LOGICAL.add("and"); LOGICAL.add("or"); LOGICAL.add("when"); LOGICAL.add("else"); LOGICAL.add("end"); - + QUANTIFIERS.add("in"); QUANTIFIERS.add("all"); QUANTIFIERS.add("exists"); QUANTIFIERS.add("some"); QUANTIFIERS.add("any"); - + DML.add("insert"); DML.add("update"); DML.add("delete"); - + MISC.add("select"); MISC.add("on"); } - + private static final String indentString = " "; private static final String initial = "\n "; public static String format(String source) { return new FormatProcess(source).perform().trim(); } - + //------------------------------------------------------------------------------------------------ private static class FormatProcess { boolean beginLine = true; boolean afterBeginBeforeEnd = false; boolean afterByOrSetOrFromOrSelect = false; -// boolean afterValues = false; + // boolean afterValues = false; boolean afterOn = false; boolean afterBetween = false; boolean afterInsert = false; diff --git a/hutool-db/src/test/java/cn/hutool/db/DbTest.java b/hutool-db/src/test/java/cn/hutool/db/DbTest.java index 816305b39..ca4ed605a 100644 --- a/hutool-db/src/test/java/cn/hutool/db/DbTest.java +++ b/hutool-db/src/test/java/cn/hutool/db/DbTest.java @@ -37,6 +37,25 @@ public class DbTest { Assert.assertEquals(1, page1.size()); } + @Test + public void pageTest2() throws SQLException { + String sql = "select * from user"; + // 测试数据库中一共4条数据,第0页有3条,第1页有1条 + List page0 = Db.use().page( + sql, Page.of(0, 3)); + Assert.assertEquals(3, page0.size()); + + List page1 = Db.use().page( + sql, Page.of(1, 3)); + Assert.assertEquals(1, page1.size()); + } + + @Test + public void countTest() throws SQLException { + final long count = Db.use().count("select * from user"); + Assert.assertEquals(4, count); + } + @Test public void findLikeTest() throws SQLException { // 方式1 From 54749abfccc40acf36e344d487dc72a07fc860dc Mon Sep 17 00:00:00 2001 From: Looly Date: Sun, 6 Dec 2020 07:51:55 +0800 Subject: [PATCH 14/47] fix comment --- hutool-all/src/main/java/cn/hutool/Hutool.java | 1 + hutool-db/src/main/java/cn/hutool/db/DialectRunner.java | 1 + hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java | 1 + 3 files changed, 3 insertions(+) diff --git a/hutool-all/src/main/java/cn/hutool/Hutool.java b/hutool-all/src/main/java/cn/hutool/Hutool.java index 891b5b499..c35dce5f7 100644 --- a/hutool-all/src/main/java/cn/hutool/Hutool.java +++ b/hutool-all/src/main/java/cn/hutool/Hutool.java @@ -45,6 +45,7 @@ public class Hutool { /** * 显示Hutool所有的工具类 * + * @return 工具类名集合 * @since 5.5.2 */ public static Set> getAllUtils() { diff --git a/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java b/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java index 799ce1d31..4bbe15a51 100644 --- a/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java +++ b/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java @@ -85,6 +85,7 @@ public class DialectRunner implements Serializable { * * @param conn 数据库连接 * @param record 记录 + * @param generatedKeysHandler 自增主键处理器,用于定义返回自增主键的范围和类型 * @return 主键列表 * @throws SQLException SQL执行异常 */ diff --git a/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java b/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java index 212a4090b..067b36665 100644 --- a/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java +++ b/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java @@ -383,6 +383,7 @@ public class SqlConnRunner extends DialectRunner { * @param fields 返回的字段列表,null则返回所有字段 * @param where 条件实体类(包含表名) * @param page 分页对象 + * @param handler 结果集处理器 * @return 结果对象 * @throws SQLException SQL执行异常 */ From 824aaa2618baa2ffc89634d3539a5729d7fd5494 Mon Sep 17 00:00:00 2001 From: haibinxiao Date: Sun, 6 Dec 2020 19:27:02 +0800 Subject: [PATCH 15/47] =?UTF-8?q?=E6=95=8F=E6=84=9F=E8=AF=8D=E8=BF=87?= =?UTF-8?q?=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/cn/hutool/dfa/FoundWord.java | 50 ++++++ .../cn/hutool/dfa/SensitiveProcessor.java | 23 +++ .../java/cn/hutool/dfa/SensitiveUtil.java | 145 +++++++++++++----- .../src/main/java/cn/hutool/dfa/WordTree.java | 24 ++- .../test/java/cn/hutool/dfa/test/DfaTest.java | 41 ++--- .../cn/hutool/dfa/test/SensitiveUtilTest.java | 49 ++++++ 6 files changed, 257 insertions(+), 75 deletions(-) create mode 100644 hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java create mode 100644 hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java create mode 100644 hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java b/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java new file mode 100644 index 000000000..b24fc2232 --- /dev/null +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java @@ -0,0 +1,50 @@ +package cn.hutool.dfa; + +/** + * @author 肖海斌 + * @Date 2020-12-05 + *

+ * 匹配到的敏感词,包含敏感词,text中匹配敏感词的内容,以及匹配内容在text中的下标, + * 下标可以用来做敏感词的进一步处理,如果替换成** + */ +public class FoundWord { + /** + * 生效的敏感词 + */ + private String word; + /** + * 敏感词匹配到的内容 + */ + private String foundWord; + /** + * 匹配内容在待分析字符串中的开始位置 + */ + private int startIndex; + /** + * 匹配内容在待分析字符串中的结束位置 + */ + private int endIndex; + + public FoundWord(String word, String foundWord, int start, int end) { + this.word = word; + this.foundWord = foundWord; + this.startIndex = start; + this.endIndex = end; + } + + public String getWord() { + return word; + } + + public String getFoundWord() { + return foundWord; + } + + public int getStartIndex() { + return startIndex; + } + + public int getEndIndex() { + return endIndex; + } +} diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java new file mode 100644 index 000000000..e8a1e8509 --- /dev/null +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java @@ -0,0 +1,23 @@ +package cn.hutool.dfa; + +/** + * @author 肖海斌 + * @Date 2020-12-05 + * 敏感词过滤处理器,默认按字符数替换成* + */ +public interface SensitiveProcessor { + + /** + * 敏感词过滤处理 + * @param foundWord 敏感词匹配到的内容 + * @return 敏感词过滤后的内容,默认按字符数替换成* + */ + default String process(FoundWord foundWord) { + int length = foundWord.getFoundWord().length(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append("*"); + } + return sb.toString(); + } +} diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java index d64100494..67244a4ad 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java @@ -1,77 +1,84 @@ package cn.hutool.dfa; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Filter; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * 敏感词工具类 - * @author Looly * + * @author Looly */ public final class SensitiveUtil { public static final char DEFAULT_SEPARATOR = StrUtil.C_COMMA; private static final WordTree sensitiveTree = new WordTree(); - + /** * @return 是否已经被初始化 */ - public static boolean isInited(){ + public static boolean isInited() { return !sensitiveTree.isEmpty(); } - + /** * 初始化敏感词树 - * @param isAsync 是否异步初始化 + * + * @param isAsync 是否异步初始化 * @param sensitiveWords 敏感词列表 */ - public static void init(final Collection sensitiveWords, boolean isAsync){ - if(isAsync){ + public static void init(final Collection sensitiveWords, boolean isAsync) { + if (isAsync) { ThreadUtil.execAsync(() -> { init(sensitiveWords); return true; }); - }else{ + } else { init(sensitiveWords); } } - + /** * 初始化敏感词树 + * * @param sensitiveWords 敏感词列表 */ - public static void init(Collection sensitiveWords){ + public static void init(Collection sensitiveWords) { sensitiveTree.clear(); sensitiveTree.addWords(sensitiveWords); // log.debug("Sensitive init finished, sensitives: {}", sensitiveWords); } - + /** * 初始化敏感词树 + * * @param sensitiveWords 敏感词列表组成的字符串 - * @param isAsync 是否异步初始化 - * @param separator 分隔符 + * @param isAsync 是否异步初始化 + * @param separator 分隔符 */ - public static void init(String sensitiveWords, char separator, boolean isAsync){ - if(StrUtil.isNotBlank(sensitiveWords)){ + public static void init(String sensitiveWords, char separator, boolean isAsync) { + if (StrUtil.isNotBlank(sensitiveWords)) { init(StrUtil.split(sensitiveWords, separator), isAsync); } } - + /** * 初始化敏感词树,使用逗号分隔每个单词 + * * @param sensitiveWords 敏感词列表组成的字符串 - * @param isAsync 是否异步初始化 + * @param isAsync 是否异步初始化 */ - public static void init(String sensitiveWords, boolean isAsync){ + public static void init(String sensitiveWords, boolean isAsync) { init(sensitiveWords, DEFAULT_SEPARATOR, isAsync); } - + /** * 设置字符过滤规则,通过定义字符串过滤规则,过滤不需要的字符
* 当accept为false时,此字符不参与匹配 @@ -80,90 +87,144 @@ public final class SensitiveUtil { * @since 5.4.4 */ public static void setCharFilter(Filter charFilter) { - if(charFilter != null) { + if (charFilter != null) { sensitiveTree.setCharFilter(charFilter); } } - + /** * 是否包含敏感词 + * * @param text 文本 * @return 是否包含 */ - public static boolean containsSensitive(String text){ + public static boolean containsSensitive(String text) { return sensitiveTree.isMatch(text); } - + /** * 是否包含敏感词 + * * @param obj bean,会被转为JSON字符串 * @return 是否包含 */ - public static boolean containsSensitive(Object obj){ + public static boolean containsSensitive(Object obj) { return sensitiveTree.isMatch(JSONUtil.toJsonStr(obj)); } - + /** * 查找敏感词,返回找到的第一个敏感词 + * * @param text 文本 * @return 敏感词 */ - public static String getFindedFirstSensitive(String text){ + public static FoundWord getFindedFirstSensitive(String text) { return sensitiveTree.match(text); } - + /** * 查找敏感词,返回找到的第一个敏感词 + * * @param obj bean,会被转为JSON字符串 * @return 敏感词 */ - public static String getFindedFirstSensitive(Object obj){ + public static FoundWord getFindedFirstSensitive(Object obj) { return sensitiveTree.match(JSONUtil.toJsonStr(obj)); } - + /** * 查找敏感词,返回找到的所有敏感词 + * * @param text 文本 * @return 敏感词 */ - public static List getFindedAllSensitive(String text){ + public static List getFindedAllSensitive(String text) { return sensitiveTree.matchAll(text); } - + /** * 查找敏感词,返回找到的所有敏感词
* 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
* 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] - * - * @param text 文本 + * + * @param text 文本 * @param isDensityMatch 是否使用密集匹配原则 - * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 * @return 敏感词 */ - public static List getFindedAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch){ + public static List getFindedAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) { return sensitiveTree.matchAll(text, -1, isDensityMatch, isGreedMatch); } - + /** * 查找敏感词,返回找到的所有敏感词 + * * @param bean 对象,会被转为JSON * @return 敏感词 */ - public static List getFindedAllSensitive(Object bean){ + public static List getFindedAllSensitive(Object bean) { return sensitiveTree.matchAll(JSONUtil.toJsonStr(bean)); } - + /** * 查找敏感词,返回找到的所有敏感词
* 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
* 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] - * - * @param bean 对象,会被转为JSON + * + * @param bean 对象,会被转为JSON * @param isDensityMatch 是否使用密集匹配原则 - * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 * @return 敏感词 */ - public static List getFindedAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch){ + public static List getFindedAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) { return getFindedAllSensitive(JSONUtil.toJsonStr(bean), isDensityMatch, isGreedMatch); } + + /** + * 敏感词过滤 + * + * @param bean 对象,会被转为JSON + * @param isGreedMatch 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * @param sensitiveProcessor 敏感词处理器,默认按匹配内容的字符数替换成* + * @param bean的class类型 + * @return 敏感词过滤处理后的bean对象 + */ + public static T sensitiveFilter(T bean, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) { + sensitiveProcessor = sensitiveProcessor == null ? new SensitiveProcessor() { + } : sensitiveProcessor; + String jsonText = JSONUtil.toJsonStr(bean); + Class c = (Class) bean.getClass(); + return JSONUtil.toBean(sensitiveFilter(jsonText, isGreedMatch, sensitiveProcessor), c); + } + + /** + * @param text 文本 + * @param isGreedMatch 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * @param sensitiveProcessor 敏感词处理器,默认按匹配内容的字符数替换成* + * @return 敏感词过滤处理后的文本 + */ + public static String sensitiveFilter(String text, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) { + if (null == text || text.trim().equals("")) { + return text; + } + //敏感词过滤场景下,不需要密集匹配 + List foundWordList = getFindedAllSensitive(text, false, isGreedMatch); + if (CollectionUtil.isEmpty(foundWordList)) { + return text; + } + Map foundWordMap = new HashMap<>(foundWordList.size()); + foundWordList.forEach(foundWord -> foundWordMap.put(foundWord.getStartIndex(), foundWord)); + int length = text.length(); + StringBuilder textStringBuilder = new StringBuilder(); + for (int i = 0; i < length; i++) { + FoundWord fw = foundWordMap.get(i); + if (fw != null) { + textStringBuilder.append(sensitiveProcessor.process(fw)); + i = fw.getEndIndex(); + } else { + textStringBuilder.append(text.charAt(i)); + } + } + return textStringBuilder.toString(); + } } diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java b/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java index 0d715b338..4e05657b9 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java @@ -5,12 +5,7 @@ import cn.hutool.core.lang.Filter; import cn.hutool.core.text.StrBuilder; import cn.hutool.core.util.StrUtil; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; /** * DFA(Deterministic Finite Automaton 确定有穷自动机) @@ -140,11 +135,11 @@ public class WordTree extends HashMap { * @param text 被检查的文本 * @return 匹配到的关键字 */ - public String match(String text) { + public FoundWord match(String text) { if (null == text) { return null; } - List matchAll = matchAll(text, 1); + List matchAll = matchAll(text, 1); if (CollectionUtil.isNotEmpty(matchAll)) { return matchAll.get(0); } @@ -159,7 +154,7 @@ public class WordTree extends HashMap { * @param text 被检查的文本 * @return 匹配的词列表 */ - public List matchAll(String text) { + public List matchAll(String text) { return matchAll(text, -1); } @@ -170,7 +165,7 @@ public class WordTree extends HashMap { * @param limit 限制匹配个数 * @return 匹配的词列表 */ - public List matchAll(String text, int limit) { + public List matchAll(String text, int limit) { return matchAll(text, limit, false, false); } @@ -185,20 +180,22 @@ public class WordTree extends HashMap { * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 * @return 匹配的词列表 */ - public List matchAll(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) { + public List matchAll(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) { if (null == text) { return null; } - List foundWords = new ArrayList<>(); + List foundWords = new ArrayList<>(); WordTree current = this; int length = text.length(); final Filter charFilter = this.charFilter; //存放查找到的字符缓存。完整出现一个词时加到findedWords中,否则清空 final StrBuilder wordBuffer = StrUtil.strBuilder(); + final StrBuilder keyBuffer = StrUtil.strBuilder(); char currentChar; for (int i = 0; i < length; i++) { wordBuffer.reset(); + keyBuffer.reset(); for (int j = i; j < length; j++) { currentChar = text.charAt(j); // Console.log("i: {}, j: {}, currentChar: {}", i, j, currentChar); @@ -216,9 +213,10 @@ public class WordTree extends HashMap { break; } wordBuffer.append(currentChar); + keyBuffer.append(currentChar); if (current.isEnd(currentChar)) { //到达单词末尾,关键词成立,从此词的下一个位置开始查找 - foundWords.add(wordBuffer.toString()); + foundWords.add(new FoundWord(keyBuffer.toString(), wordBuffer.toString(), i, j)); if (limit > 0 && foundWords.size() >= limit) { //超过匹配限制个数,直接返回 return foundWords; diff --git a/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java b/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java index 9d500f870..065b3d0da 100644 --- a/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java +++ b/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java @@ -1,16 +1,16 @@ package cn.hutool.dfa.test; -import java.util.List; - +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.dfa.FoundWord; +import cn.hutool.dfa.WordTree; import org.junit.Assert; import org.junit.Test; -import cn.hutool.core.collection.CollectionUtil; -import cn.hutool.dfa.WordTree; +import java.util.List; /** * DFA单元测试 - * + * * @author Looly * */ @@ -28,8 +28,8 @@ public class DfaTest { // 情况一:标准匹配,匹配到最短关键词,并跳过已经匹配的关键词 // 匹配到【大】,就不再继续匹配了,因此【大土豆】不匹配 // 匹配到【刚出锅】,就跳过这三个字了,因此【出锅】不匹配(由于刚首先被匹配,因此长的被匹配,最短匹配只针对第一个字相同选最短) - List matchAll = tree.matchAll(text, -1, false, false); - Assert.assertEquals(matchAll, CollectionUtil.newArrayList("大", "土^豆", "刚出锅")); + List matchAll = tree.matchAll(text, -1, false, false); + Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅")); } /** @@ -44,8 +44,8 @@ public class DfaTest { // 情况二:匹配到最短关键词,不跳过已经匹配的关键词 // 【大】被匹配,最短匹配原则【大土豆】被跳过,【土豆继续被匹配】 // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 - List matchAll = tree.matchAll(text, -1, true, false); - Assert.assertEquals(matchAll, CollectionUtil.newArrayList("大", "土^豆", "刚出锅", "出锅")); + List matchAll = tree.matchAll(text, -1, true, false); + Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅", "出锅")); } /** @@ -60,8 +60,8 @@ public class DfaTest { // 情况三:匹配到最长关键词,跳过已经匹配的关键词 // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配 // 由于【大土豆】被匹配,【土豆】被跳过,由于【刚出锅】被匹配,【出锅】被跳过 - List matchAll = tree.matchAll(text, -1, false, true); - Assert.assertEquals(matchAll, CollectionUtil.newArrayList("大", "大土^豆", "刚出锅")); + List matchAll = tree.matchAll(text, -1, false, true); + Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("大", "大土^豆", "刚出锅")); } @@ -77,8 +77,8 @@ public class DfaTest { // 情况四:匹配到最长关键词,不跳过已经匹配的关键词(最全关键词) // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配,由于不跳过已经匹配的关键词,土豆继续被匹配 // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 - List matchAll = tree.matchAll(text, -1, true, true); - Assert.assertEquals(matchAll, CollectionUtil.newArrayList("大", "大土^豆", "土^豆", "刚出锅", "出锅")); + List matchAll = tree.matchAll(text, -1, true, true); + Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("大", "大土^豆", "土^豆", "刚出锅", "出锅")); } @@ -90,23 +90,24 @@ public class DfaTest { WordTree tree = new WordTree(); tree.addWord("tio"); - List all = tree.matchAll("AAAAAAAt-ioBBBBBBB"); - Assert.assertEquals(all, CollectionUtil.newArrayList("t-io")); + List all = tree.matchAll("AAAAAAAt-ioBBBBBBB"); + Assert.assertEquals(all.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("t-io")); } @Test - public void aTest(){ + public void aTest() { WordTree tree = new WordTree(); tree.addWord("women"); String text = "a WOMEN todo.".toLowerCase(); - List matchAll = tree.matchAll(text, -1, false, false); - Assert.assertEquals("[women]", matchAll.toString()); + List matchAll = tree.matchAll(text, -1, false, false); + Assert.assertEquals("[women]", matchAll.stream().map(fw -> fw.getFoundWord()).toString()); } - + // ---------------------------------------------------------------------------------------------------------- + /** * 构建查找树 - * + * * @return 查找树 */ private WordTree buildWordTree() { diff --git a/hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java b/hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java new file mode 100644 index 000000000..8a19fef3e --- /dev/null +++ b/hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java @@ -0,0 +1,49 @@ +package cn.hutool.dfa.test; + +import cn.hutool.dfa.SensitiveUtil; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class SensitiveUtilTest { + + @Test + public void testSensitiveFilter() { + List wordList = new ArrayList<>(); + wordList.add("大"); + wordList.add("大土豆"); + wordList.add("土豆"); + wordList.add("刚出锅"); + wordList.add("出锅"); + TestBean bean = new TestBean(); + bean.setStr("我有一颗$大土^豆,刚出锅的"); + bean.setNum(100); + SensitiveUtil.init(wordList); + bean = SensitiveUtil.sensitiveFilter(bean, true, null); + Assert.assertEquals(bean.getStr(), "我有一颗$****,***的"); + } + + public static class TestBean { + private String str; + private Integer num; + + public String getStr() { + return str; + } + + public void setStr(String str) { + this.str = str; + } + + public Integer getNum() { + return num; + } + + public void setNum(Integer num) { + this.num = num; + } + } + +} From f7c640934d6bf4c91fc11d0291f82f8eb3937f2c Mon Sep 17 00:00:00 2001 From: haibinxiao Date: Sun, 6 Dec 2020 21:29:21 +0800 Subject: [PATCH 16/47] =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java | 1 - hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java | 1 - 2 files changed, 2 deletions(-) diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java b/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java index b24fc2232..e57a0f9ec 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java @@ -2,7 +2,6 @@ package cn.hutool.dfa; /** * @author 肖海斌 - * @Date 2020-12-05 *

* 匹配到的敏感词,包含敏感词,text中匹配敏感词的内容,以及匹配内容在text中的下标, * 下标可以用来做敏感词的进一步处理,如果替换成** diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java index e8a1e8509..34c0128b8 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveProcessor.java @@ -2,7 +2,6 @@ package cn.hutool.dfa; /** * @author 肖海斌 - * @Date 2020-12-05 * 敏感词过滤处理器,默认按字符数替换成* */ public interface SensitiveProcessor { From f5c53a8f60c7f51866ab47601bd1cdcdb05822f7 Mon Sep 17 00:00:00 2001 From: haibinxiao Date: Sun, 6 Dec 2020 22:00:12 +0800 Subject: [PATCH 17/47] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E8=A7=A3=E5=86=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/java/cn/hutool/dfa/test/DfaTest.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java b/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java index 065b3d0da..913f10fce 100644 --- a/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java +++ b/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java @@ -7,6 +7,7 @@ import org.junit.Assert; import org.junit.Test; import java.util.List; +import java.util.stream.Collectors; /** * DFA单元测试 @@ -29,7 +30,7 @@ public class DfaTest { // 匹配到【大】,就不再继续匹配了,因此【大土豆】不匹配 // 匹配到【刚出锅】,就跳过这三个字了,因此【出锅】不匹配(由于刚首先被匹配,因此长的被匹配,最短匹配只针对第一个字相同选最短) List matchAll = tree.matchAll(text, -1, false, false); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅")); + Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅")); } /** @@ -45,7 +46,7 @@ public class DfaTest { // 【大】被匹配,最短匹配原则【大土豆】被跳过,【土豆继续被匹配】 // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 List matchAll = tree.matchAll(text, -1, true, false); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅", "出锅")); + Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅", "出锅")); } /** @@ -61,7 +62,7 @@ public class DfaTest { // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配 // 由于【大土豆】被匹配,【土豆】被跳过,由于【刚出锅】被匹配,【出锅】被跳过 List matchAll = tree.matchAll(text, -1, false, true); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("大", "大土^豆", "刚出锅")); + Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "大土^豆", "刚出锅")); } @@ -78,7 +79,7 @@ public class DfaTest { // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配,由于不跳过已经匹配的关键词,土豆继续被匹配 // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 List matchAll = tree.matchAll(text, -1, true, true); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("大", "大土^豆", "土^豆", "刚出锅", "出锅")); + Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "大土^豆", "土^豆", "刚出锅", "出锅")); } @@ -91,7 +92,7 @@ public class DfaTest { tree.addWord("tio"); List all = tree.matchAll("AAAAAAAt-ioBBBBBBB"); - Assert.assertEquals(all.stream().map(fw -> fw.getFoundWord()), CollectionUtil.newArrayList("t-io")); + Assert.assertEquals(all.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("t-io")); } @Test @@ -100,7 +101,7 @@ public class DfaTest { tree.addWord("women"); String text = "a WOMEN todo.".toLowerCase(); List matchAll = tree.matchAll(text, -1, false, false); - Assert.assertEquals("[women]", matchAll.stream().map(fw -> fw.getFoundWord()).toString()); + Assert.assertEquals("[women]", matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()).toString()); } // ---------------------------------------------------------------------------------------------------------- From 11edc1fcc666537aed6f818405ff4ce5425e428c Mon Sep 17 00:00:00 2001 From: haibinxiao Date: Sun, 6 Dec 2020 23:02:11 +0800 Subject: [PATCH 18/47] =?UTF-8?q?sensitiveProcessor=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=88=B0=E6=AD=A3=E7=A1=AE=E7=9A=84=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java index 67244a4ad..6a396d49d 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java @@ -190,8 +190,6 @@ public final class SensitiveUtil { * @return 敏感词过滤处理后的bean对象 */ public static T sensitiveFilter(T bean, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) { - sensitiveProcessor = sensitiveProcessor == null ? new SensitiveProcessor() { - } : sensitiveProcessor; String jsonText = JSONUtil.toJsonStr(bean); Class c = (Class) bean.getClass(); return JSONUtil.toBean(sensitiveFilter(jsonText, isGreedMatch, sensitiveProcessor), c); @@ -212,6 +210,8 @@ public final class SensitiveUtil { if (CollectionUtil.isEmpty(foundWordList)) { return text; } + sensitiveProcessor = sensitiveProcessor == null ? new SensitiveProcessor() { + } : sensitiveProcessor; Map foundWordMap = new HashMap<>(foundWordList.size()); foundWordList.forEach(foundWord -> foundWordMap.put(foundWord.getStartIndex(), foundWord)); int length = text.length(); From 97f1e8b3ddbb67e5e358fae98ae1459596dbc2ba Mon Sep 17 00:00:00 2001 From: Looly Date: Mon, 7 Dec 2020 18:10:16 +0800 Subject: [PATCH 19/47] fix localdate for json bug --- CHANGELOG.md | 5 +++- .../cn/hutool/core/collection/CollUtil.java | 6 ++--- .../core/date/TemporalAccessorUtil.java | 15 +++++++++++- .../core/date/TemporalAccessorUtilTest.java | 23 +++++++++++++++++++ .../java/cn/hutool/json/InternalJSONUtil.java | 7 ++++-- 5 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/TemporalAccessorUtilTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0042e17dd..94e5c3c1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------------------------------------------- -# 5.5.3 (2020-12-06) +# 5.5.3 (2020-12-07) ### 新特性 * 【core 】 IdcardUtil增加行政区划83(issue#1277@Github) @@ -14,6 +14,9 @@ ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) * 【poi 】 修复sax读取自定义格式单元格无法识别日期类型的问题(issue#1283@Github) +* 【core 】 修复CollUtil.get越界问题(issue#1292@Github) +* 【json 】 修复TemporalAccessorUtil无法格式化LocalDate带时间问题(issue#1289@Github) +* 【json 】 修复自定义日期格式的LocalDateTime没有包装引号问题(issue#1289@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java index b56c239bd..aa73d40f5 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java @@ -1870,7 +1870,7 @@ public class CollUtil { /** * Iterator转换为Enumeration *

- * Adapt the specified Iterator to the Enumeration interface. + * Adapt the specified {@link Iterator} to the {@link Enumeration} interface. * * @param 集合元素类型 * @param iter {@link Iterator} @@ -1883,7 +1883,7 @@ public class CollUtil { /** * Enumeration转换为Iterator *

- * Adapt the specified Enumeration to the Iterator interface + * Adapt the specified {@code Enumeration} to the {@code Iterator} interface * * @param 集合元素类型 * @param e {@link Enumeration} @@ -2186,7 +2186,7 @@ public class CollUtil { } // 检查越界 - if (index >= size) { + if (index >= size || index < 0) { return null; } diff --git a/hutool-core/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java index 3ecb8a5af..9d838f861 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java @@ -13,6 +13,7 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalField; +import java.time.temporal.UnsupportedTemporalTypeException; /** * {@link TemporalAccessor} 工具类封装 @@ -54,7 +55,19 @@ public class TemporalAccessorUtil extends TemporalUtil{ formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; } - return formatter.format(time); + + try { + return formatter.format(time); + } catch (UnsupportedTemporalTypeException e){ + if(time instanceof LocalDate && e.getMessage().contains("HourOfDay")){ + // 用户传入LocalDate,但是要求格式化带有时间部分,转换为LocalDateTime重试 + return formatter.format(((LocalDate) time).atStartOfDay()); + }else if(time instanceof LocalTime && e.getMessage().contains("YearOfEra")){ + // 用户传入LocalTime,但是要求格式化带有日期部分,转换为LocalDateTime重试 + return formatter.format(((LocalTime) time).atDate(LocalDate.now())); + } + throw e; + } } /** diff --git a/hutool-core/src/test/java/cn/hutool/core/date/TemporalAccessorUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/date/TemporalAccessorUtilTest.java new file mode 100644 index 000000000..bf8501625 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/date/TemporalAccessorUtilTest.java @@ -0,0 +1,23 @@ +package cn.hutool.core.date; + +import org.junit.Assert; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.LocalTime; + +public class TemporalAccessorUtilTest { + + @Test + public void formatLocalDateTest(){ + final String format = TemporalAccessorUtil.format(LocalDate.of(2020, 12, 7), DatePattern.NORM_DATETIME_PATTERN); + Assert.assertEquals("2020-12-07 00:00:00", format); + } + + @Test + public void formatLocalTimeTest(){ + final String today = TemporalAccessorUtil.format(LocalDate.now(), DatePattern.NORM_DATE_PATTERN); + final String format = TemporalAccessorUtil.format(LocalTime.MIN, DatePattern.NORM_DATETIME_PATTERN); + Assert.assertEquals(today + " 00:00:00", format); + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java index 98e9c3264..177d97b06 100644 --- a/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java +++ b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java @@ -240,11 +240,14 @@ final class InternalJSONUtil { */ private static String formatDate(Object dateObj, String format) { if (StrUtil.isNotBlank(format)) { + final String dateStr; if(dateObj instanceof TemporalAccessor){ - return TemporalAccessorUtil.format((TemporalAccessor) dateObj, format); + dateStr = TemporalAccessorUtil.format((TemporalAccessor) dateObj, format); + } else{ + dateStr = DateUtil.format(Convert.toDate(dateObj), format); } //用户定义了日期格式 - return JSONUtil.quote(DateUtil.format(Convert.toDate(dateObj), format)); + return JSONUtil.quote(dateStr); } //默认使用时间戳 From 775afa68cc7fac80b266e68d3777fc49c0629263 Mon Sep 17 00:00:00 2001 From: Looly Date: Mon, 7 Dec 2020 18:39:21 +0800 Subject: [PATCH 20/47] fix lock bug --- CHANGELOG.md | 3 ++- .../src/main/java/cn/hutool/cache/impl/AbstractCache.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e5c3c1f..d8259f1f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,9 @@ * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) * 【poi 】 修复sax读取自定义格式单元格无法识别日期类型的问题(issue#1283@Github) * 【core 】 修复CollUtil.get越界问题(issue#1292@Github) -* 【json 】 修复TemporalAccessorUtil无法格式化LocalDate带时间问题(issue#1289@Github) +* 【core 】 修复TemporalAccessorUtil无法格式化LocalDate带时间问题(issue#1289@Github) * 【json 】 修复自定义日期格式的LocalDateTime没有包装引号问题(issue#1289@Github) +* 【cache 】 get中unlock改为unlockRead(issue#1294@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java index f5e583f82..4269dc312 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java @@ -174,7 +174,7 @@ public abstract class AbstractCache implements Cache { return co.get(isUpdateLastAccess); } } finally { - lock.unlock(stamp); + lock.unlockRead(stamp); } // 过期 From ea070bf83e08c6e3a31c6cfc9785199f24133641 Mon Sep 17 00:00:00 2001 From: Looly Date: Mon, 7 Dec 2020 19:06:23 +0800 Subject: [PATCH 21/47] change name --- CHANGELOG.md | 2 ++ .../main/java/cn/hutool/cache/impl/CacheObj.java | 5 ++--- .../src/main/java/cn/hutool/json/JSONConfig.java | 3 ++- .../src/main/java/cn/hutool/json/JSONObject.java | 13 +++++++------ .../{JSONStrFormater.java => JSONStrFormatter.java} | 2 +- .../src/main/java/cn/hutool/json/JSONUtil.java | 2 +- .../java/cn/hutool/json/JSONStrFormaterTest.java | 6 +++--- 7 files changed, 18 insertions(+), 15 deletions(-) rename hutool-json/src/main/java/cn/hutool/json/{JSONStrFormater.java => JSONStrFormatter.java} (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8259f1f5..6006c1758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ * 【core 】 multipart中int改为long,解决大文件上传越界问题(issue#I27WZ3@Gitee) * 【core 】 ListUtil.page增加检查(pr#224@Gitee) * 【db 】 Db增加使用sql的page方法(issue#247@Gitee) +* 【cache 】 CacheObj的isExpired()逻辑修改(issue#1295@Github) +* 【json 】 JSONStrFormater改为JSONStrFormatter ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java index 1a8f08215..e8fa48348 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java @@ -44,9 +44,8 @@ public class CacheObj implements Serializable{ */ boolean isExpired() { if(this.ttl > 0) { - final long expiredTime = this.lastAccess + this.ttl; - // expiredTime > 0 杜绝Long类型溢出变负数问题,当当前时间超过过期时间,表示过期 - return expiredTime > 0 && expiredTime < System.currentTimeMillis(); + // 此处不考虑时间回拨 + return (System.currentTimeMillis() - this.lastAccess) > this.ttl; } return false; } diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONConfig.java b/hutool-json/src/main/java/cn/hutool/json/JSONConfig.java index fd558ae82..6f4be20a6 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONConfig.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONConfig.java @@ -115,7 +115,8 @@ public class JSONConfig implements Serializable { } /** - * 设置日期格式,null表示默认的时间戳 + * 设置日期格式,null表示默认的时间戳
+ * 此方法设置的日期格式仅对转换为JSON字符串有效,对解析JSON为bean无效。 * * @param dateFormat 日期格式,null表示默认的时间戳 * @return this diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONObject.java b/hutool-json/src/main/java/cn/hutool/json/JSONObject.java index 67a5e20ae..33a087df4 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONObject.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONObject.java @@ -261,7 +261,8 @@ public class JSONObject implements JSON, JSONGetter, Map } /** - * 设置转为字符串时的日期格式,默认为时间戳(null值) + * 设置转为字符串时的日期格式,默认为时间戳(null值)
+ * 此方法设置的日期格式仅对转换为JSON字符串有效,对解析JSON为bean无效。 * * @param format 格式,null表示使用时间戳 * @return this @@ -341,7 +342,7 @@ public class JSONObject implements JSON, JSONGetter, Map } /** - * PUT 键值对到JSONObject中,在忽略null模式下,如果值为null,将此键移除 + * PUT 键值对到JSONObject中,在忽略null模式下,如果值为{@code null},将此键移除 * * @param key 键 * @param value 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL. @@ -356,7 +357,7 @@ public class JSONObject implements JSON, JSONGetter, Map } /** - * 设置键值对到JSONObject中,在忽略null模式下,如果值为null,将此键移除 + * 设置键值对到JSONObject中,在忽略null模式下,如果值为{@code null},将此键移除 * * @param key 键 * @param value 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL. @@ -426,7 +427,7 @@ public class JSONObject implements JSON, JSONGetter, Map * @param key 键 * @param value 被积累的值 * @return this. - * @throws JSONException 如果给定键为null或者键对应的值存在且为非JSONArray + * @throws JSONException 如果给定键为{@code null}或者键对应的值存在且为非JSONArray */ public JSONObject accumulate(String key, Object value) throws JSONException { InternalJSONUtil.testValidity(value); @@ -447,7 +448,7 @@ public class JSONObject implements JSON, JSONGetter, Map * @param key 键 * @param value 值 * @return this. - * @throws JSONException 如果给定键为null或者键对应的值存在且为非JSONArray + * @throws JSONException 如果给定键为{@code null}或者键对应的值存在且为非JSONArray */ public JSONObject append(String key, Object value) throws JSONException { InternalJSONUtil.testValidity(value); @@ -545,7 +546,7 @@ public class JSONObject implements JSON, JSONGetter, Map /** * 返回JSON字符串
- * 如果解析错误,返回null + * 如果解析错误,返回{@code null} * * @return JSON字符串 */ diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONStrFormater.java b/hutool-json/src/main/java/cn/hutool/json/JSONStrFormatter.java similarity index 95% rename from hutool-json/src/main/java/cn/hutool/json/JSONStrFormater.java rename to hutool-json/src/main/java/cn/hutool/json/JSONStrFormatter.java index 25ac94329..4a319778b 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONStrFormater.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONStrFormatter.java @@ -9,7 +9,7 @@ import cn.hutool.core.util.StrUtil; * @author looly * @since 3.1.2 */ -public class JSONStrFormater { +public class JSONStrFormatter { /** 单位缩进字符串。*/ private static final String SPACE = " "; diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java b/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java index d91ff3afd..ad3948af2 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java @@ -776,7 +776,7 @@ public final class JSONUtil { * @since 3.1.2 */ public static String formatJsonStr(String jsonStr) { - return JSONStrFormater.format(jsonStr); + return JSONStrFormatter.format(jsonStr); } /** diff --git a/hutool-json/src/test/java/cn/hutool/json/JSONStrFormaterTest.java b/hutool-json/src/test/java/cn/hutool/json/JSONStrFormaterTest.java index e9450f10e..b15a1c80c 100644 --- a/hutool-json/src/test/java/cn/hutool/json/JSONStrFormaterTest.java +++ b/hutool-json/src/test/java/cn/hutool/json/JSONStrFormaterTest.java @@ -13,21 +13,21 @@ public class JSONStrFormaterTest { @Test public void formatTest() { String json = "{'age':23,'aihao':['pashan','movies'],'name':{'firstName':'zhang','lastName':'san','aihao':['pashan','movies','name':{'firstName':'zhang','lastName':'san','aihao':['pashan','movies']}]}}"; - String result = JSONStrFormater.format(json); + String result = JSONStrFormatter.format(json); Assert.assertNotNull(result); } @Test public void formatTest2() { String json = "{\"abc\":{\"def\":\"\\\"[ghi]\"}}"; - String result = JSONStrFormater.format(json); + String result = JSONStrFormatter.format(json); Assert.assertNotNull(result); } @Test public void formatTest3() { String json = "{\"id\":13,\"title\":\"《标题》\",\"subtitle\":\"副标题z'c'z'xv'c'xv\",\"user_id\":6,\"type\":0}"; - String result = JSONStrFormater.format(json); + String result = JSONStrFormatter.format(json); Assert.assertNotNull(result); } } From 04a3250708f70383c054da9ddabccf0664c23b21 Mon Sep 17 00:00:00 2001 From: Looly Date: Mon, 7 Dec 2020 19:13:46 +0800 Subject: [PATCH 22/47] fix code --- hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java | 4 ++-- hutool-json/src/main/java/cn/hutool/json/JSONGetter.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java index 365b42b0d..10902ac94 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java @@ -1339,8 +1339,8 @@ public class DateUtil extends CalendarUtil { *

 	 * 有时候我们计算相差天数的时候需要忽略时分秒。
 	 * 比如:2016-02-01 23:59:59和2016-02-02 00:00:00相差一秒
-	 * 如果isReset为false相差天数为0。
-	 * 如果isReset为true相差天数将被计算为1
+	 * 如果isReset为{@code false}相差天数为0。
+	 * 如果isReset为{@code true}相差天数将被计算为1
 	 * 
* * @param beginDate 起始日期 diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONGetter.java b/hutool-json/src/main/java/cn/hutool/json/JSONGetter.java index 7269f54a7..183bf2086 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONGetter.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONGetter.java @@ -20,10 +20,10 @@ public interface JSONGetter extends OptNullBasicTypeFromObjectGetter { JSONConfig getConfig(); /** - * key对应值是否为null或无此key + * key对应值是否为{@code null}或无此key * * @param key 键 - * @return true 无此key或值为null或{@link JSONNull#NULL}返回false,其它返回true + * @return true 无此key或值为{@code null}或{@link JSONNull#NULL}返回{@code false},其它返回{@code true} */ default boolean isNull(K key) { return JSONNull.NULL.equals(this.getObj(key)); From 850c766213344d57696acfdc3defd36789beed70 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 8 Dec 2020 04:54:44 +0800 Subject: [PATCH 23/47] add FoundWord --- CHANGELOG.md | 3 +- .../cn/hutool/core/lang/DefaultSegment.java | 34 +++++ .../java/cn/hutool/core/lang/Segment.java | 41 ++++++ .../java/cn/hutool/core/text/StrBuilder.java | 8 +- .../java/cn/hutool/core/util/ObjectUtil.java | 15 ++- .../java/cn/hutool/core/util/StrUtil.java | 106 ++++++++-------- .../main/java/cn/hutool/dfa/FoundWord.java | 76 ++++++----- .../java/cn/hutool/dfa/SensitiveUtil.java | 118 +++++++++++++++--- .../src/main/java/cn/hutool/dfa/WordTree.java | 93 +++++++++++--- .../cn/hutool/dfa/{test => }/DfaTest.java | 36 +++--- .../dfa/{test => }/SensitiveUtilTest.java | 3 +- .../main/java/cn/hutool/json/JSONUtil.java | 4 +- 12 files changed, 389 insertions(+), 148 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/DefaultSegment.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Segment.java rename hutool-dfa/src/test/java/cn/hutool/dfa/{test => }/DfaTest.java (66%) rename hutool-dfa/src/test/java/cn/hutool/dfa/{test => }/SensitiveUtilTest.java (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6006c1758..97a428e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------------------------------------------- -# 5.5.3 (2020-12-07) +# 5.5.3 (2020-12-08) ### 新特性 * 【core 】 IdcardUtil增加行政区划83(issue#1277@Github) @@ -12,6 +12,7 @@ * 【db 】 Db增加使用sql的page方法(issue#247@Gitee) * 【cache 】 CacheObj的isExpired()逻辑修改(issue#1295@Github) * 【json 】 JSONStrFormater改为JSONStrFormatter +* 【dfa 】 增加FoundWord(pr#1290@Github) ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/DefaultSegment.java b/hutool-core/src/main/java/cn/hutool/core/lang/DefaultSegment.java new file mode 100644 index 000000000..9337ec9cd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/DefaultSegment.java @@ -0,0 +1,34 @@ +package cn.hutool.core.lang; + +/** + * 片段默认实现 + * + * @param 数字类型,用于表示位置index + * @author looly + * @since 5.5.3 + */ +public class DefaultSegment implements Segment { + + protected T startIndex; + protected T endIndex; + + /** + * 构造 + * @param startIndex 起始位置 + * @param endIndex 结束位置 + */ + public DefaultSegment(T startIndex, T endIndex) { + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + @Override + public T getStartIndex() { + return this.startIndex; + } + + @Override + public T getEndIndex() { + return this.endIndex; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Segment.java b/hutool-core/src/main/java/cn/hutool/core/lang/Segment.java new file mode 100644 index 000000000..869386f46 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Segment.java @@ -0,0 +1,41 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.NumberUtil; + +import java.lang.reflect.Type; + +/** + * 片段表示,用于表示文本、集合等数据结构的一个区间。 + * @param 数字类型,用于表示位置index + * + * @author looly + * @since 5.5.3 + */ +public interface Segment { + + /** + * 获取起始位置 + * + * @return 起始位置 + */ + T getStartIndex(); + + /** + * 获取结束位置 + * + * @return 结束位置 + */ + T getEndIndex(); + + /** + * 片段长度,默认计算方法为abs({@link #getEndIndex()} - {@link #getEndIndex()}) + * + * @return 片段长度 + */ + default T length(){ + final T start = Assert.notNull(getStartIndex(), "Start index must be not null!"); + final T end = Assert.notNull(getEndIndex(), "End index must be not null!"); + return Convert.convert((Type) start.getClass(), NumberUtil.sub(end, start).abs()); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java index dae62696b..7fb068a01 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java @@ -523,7 +523,8 @@ public class StrBuilder implements CharSequence, Appendable, Serializable { * @param minimumCapacity 最小容量 */ private void ensureCapacity(int minimumCapacity) { - if (minimumCapacity > value.length) { + // overflow-conscious code + if (minimumCapacity - value.length < 0) { expandCapacity(minimumCapacity); } } @@ -535,8 +536,9 @@ public class StrBuilder implements CharSequence, Appendable, Serializable { * @param minimumCapacity 需要扩展的最小容量 */ private void expandCapacity(int minimumCapacity) { - int newCapacity = value.length * 2 + 2; - if (newCapacity < minimumCapacity) { + int newCapacity = (value.length << 1) + 2; + // overflow-conscious code + if (newCapacity - minimumCapacity < 0) { newCapacity = minimumCapacity; } if (newCapacity < 0) { diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java index 8ae304433..0cd995851 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java @@ -383,7 +383,7 @@ public class ObjectUtil { * 克隆对象
* 如果对象实现Cloneable接口,调用其clone方法
* 如果实现Serializable接口,执行深度克隆
- * 否则返回null + * 否则返回{@code null} * * @param 对象类型 * @param obj 被克隆对象 @@ -606,11 +606,24 @@ public class ObjectUtil { return ArrayUtil.emptyCount(objs); } + /** + * 是否存在{@code null}对象,通过{@link ObjectUtil#isNull(Object)} 判断元素 + * + * @param objs 被检查对象 + * @return 是否存在 + * @since 5.5.3 + * @see ArrayUtil#hasNull(Object[]) + */ + public static boolean hasNull(Object... objs) { + return ArrayUtil.hasNull(objs); + } + /** * 是否存在{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 * * @param objs 被检查对象 * @return 是否存在 + * @see ArrayUtil#hasEmpty(Object...) */ public static boolean hasEmpty(Object... objs) { return ArrayUtil.hasEmpty(objs); diff --git a/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java index cb4cb4b85..868aeca3f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java @@ -105,7 +105,7 @@ public class StrUtil { public static final char C_COLON = CharUtil.COLON; /** - * 字符常量:艾特 '@' + * 字符常量:艾特 {@code '@'} */ public static final char C_AT = CharUtil.AT; @@ -210,7 +210,7 @@ public class StrUtil { public static final String COLON = ":"; /** - * 字符串常量:艾特 "@" + * 字符串常量:艾特 {@code "@"} */ public static final String AT = "@"; @@ -246,7 +246,7 @@ public class StrUtil { public static final String HTML_GT = ">"; /** - * 字符串常量:空 JSON "{}" + * 字符串常量:空 JSON {@code "{}"} */ public static final String EMPTY_JSON = "{}"; @@ -542,7 +542,7 @@ public class StrUtil { } /** - * 如果字符串是 null,则返回指定默认字符串,否则返回字符串本身。 + * 如果字符串是 {@code null},则返回指定默认字符串,否则返回字符串本身。 * *
 	 * nullToDefault(null, "default")  = "default"
@@ -560,7 +560,7 @@ public class StrUtil {
 	}
 
 	/**
-	 * 如果字符串是null或者"",则返回指定默认字符串,否则返回字符串本身。
+	 * 如果字符串是{@code null}或者"",则返回指定默认字符串,否则返回字符串本身。
 	 *
 	 * 
 	 * emptyToDefault(null, "default")  = "default"
@@ -579,7 +579,7 @@ public class StrUtil {
 	}
 
 	/**
-	 * 如果字符串是null或者""或者空白,则返回指定默认字符串,否则返回字符串本身。
+	 * 如果字符串是{@code null}或者""或者空白,则返回指定默认字符串,否则返回字符串本身。
 	 *
 	 * 
 	 * emptyToDefault(null, "default")  = "default"
@@ -598,7 +598,7 @@ public class StrUtil {
 	}
 
 	/**
-	 * 当给定字符串为空字符串时,转换为null
+	 * 当给定字符串为空字符串时,转换为{@code null}
 	 *
 	 * @param str 被转换的字符串
 	 * @return 转换后的字符串
@@ -774,10 +774,10 @@ public class StrUtil {
 	// ------------------------------------------------------------------------ Trim
 
 	/**
-	 * 除去字符串头尾部的空白,如果字符串是null,依然返回null。
+	 * 除去字符串头尾部的空白,如果字符串是{@code null},依然返回{@code null}。
 	 *
 	 * 

- * 注意,和String.trim不同,此方法使用NumberUtil.isBlankChar 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 * *

 	 * trim(null)          = null
@@ -788,7 +788,7 @@ public class StrUtil {
 	 * 
* * @param str 要处理的字符串 - * @return 除去头尾空白的字符串,如果原字串为null,则返回null + * @return 除去头尾空白的字符串,如果原字串为{@code null},则返回{@code null} */ public static String trim(CharSequence str) { return (null == str) ? null : trim(str, 0); @@ -813,7 +813,7 @@ public class StrUtil { } /** - * 除去字符串头尾部的空白,如果字符串是{@code null},返回""。 + * 除去字符串头尾部的空白,如果字符串是{@code null},返回{@code ""}。 * *
 	 * StrUtil.trimToEmpty(null)          = ""
@@ -852,10 +852,10 @@ public class StrUtil {
 	}
 
 	/**
-	 * 除去字符串头部的空白,如果字符串是null,则返回null。
+	 * 除去字符串头部的空白,如果字符串是{@code null},则返回{@code null}。
 	 *
 	 * 

- * 注意,和String.trim不同,此方法使用CharUtil.isBlankChar 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 * *

 	 * trimStart(null)         = null
@@ -867,17 +867,17 @@ public class StrUtil {
 	 * 
* * @param str 要处理的字符串 - * @return 除去空白的字符串,如果原字串为null或结果字符串为"",则返回 null + * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} */ public static String trimStart(CharSequence str) { return trim(str, -1); } /** - * 除去字符串尾部的空白,如果字符串是null,则返回null。 + * 除去字符串尾部的空白,如果字符串是{@code null},则返回{@code null}。 * *

- * 注意,和String.trim不同,此方法使用CharUtil.isBlankChar 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 * *

 	 * trimEnd(null)       = null
@@ -889,47 +889,45 @@ public class StrUtil {
 	 * 
* * @param str 要处理的字符串 - * @return 除去空白的字符串,如果原字串为null或结果字符串为"",则返回 null + * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} */ public static String trimEnd(CharSequence str) { return trim(str, 1); } /** - * 除去字符串头尾部的空白符,如果字符串是null,依然返回null。 + * 除去字符串头尾部的空白符,如果字符串是{@code null},依然返回{@code null}。 * * @param str 要处理的字符串 - * @param mode -1表示trimStart,0表示trim全部, 1表示trimEnd - * @return 除去指定字符后的的字符串,如果原字串为null,则返回null + * @param mode {@code -1}表示trimStart,{@code 0}表示trim全部, {@code 1}表示trimEnd + * @return 除去指定字符后的的字符串,如果原字串为{@code null},则返回{@code null} */ public static String trim(CharSequence str, int mode) { + String result; if (str == null) { - return null; - } - - int length = str.length(); - int start = 0; - int end = length; - - // 扫描字符串头部 - if (mode <= 0) { - while ((start < end) && (CharUtil.isBlankChar(str.charAt(start)))) { - start++; + result = null; + } else { + int length = str.length(); + int start = 0; + int end = length;// 扫描字符串头部 + if (mode <= 0) { + while ((start < end) && (CharUtil.isBlankChar(str.charAt(start)))) { + start++; + } + }// 扫描字符串尾部 + if (mode >= 0) { + while ((start < end) && (CharUtil.isBlankChar(str.charAt(end - 1)))) { + end--; + } + } + if ((start > 0) || (end < length)) { + result = str.toString().substring(start, end); + } else { + result = str.toString(); } } - // 扫描字符串尾部 - if (mode >= 0) { - while ((start < end) && (CharUtil.isBlankChar(str.charAt(end - 1)))) { - end--; - } - } - - if ((start > 0) || (end < length)) { - return str.toString().substring(start, end); - } - - return str.toString(); + return result; } /** @@ -1251,7 +1249,7 @@ public class StrUtil { } /** - * 是否包含特定字符,忽略大小写,如果给定两个参数都为null,返回true + * 是否包含特定字符,忽略大小写,如果给定两个参数都为{@code null},返回true * * @param str 被检测字符串 * @param testStr 被测试是否包含的字符串 @@ -1971,7 +1969,7 @@ public class StrUtil { * abcdefgh 2 3 =》 c
* abcdefgh 2 -3 =》 cde
* - * @param str String + * @param str String * @param fromIndexInclude 开始的index(包括) * @param toIndexExclude 结束的index(不包括) * @return 字串 @@ -2094,7 +2092,7 @@ public class StrUtil { /** * 切割指定位置之前部分的字符串 * - * @param string 字符串 + * @param string 字符串 * @param toIndexExclude 切割到的位置(不包括) * @return 切割后的剩余的前半部分字符串 */ @@ -2406,12 +2404,12 @@ public class StrUtil { final List result = new LinkedList<>(); final String[] split = split(str, prefix); - if(prefix.equals(suffix)){ + if (prefix.equals(suffix)) { // 前后缀字符相同,单独处理 for (int i = 1, length = split.length - 1; i < length; i += 2) { result.add(split[i]); } - } else{ + } else { int suffixIndex; for (String fragment : split) { suffixIndex = fragment.indexOf(suffix.toString()); @@ -2441,7 +2439,7 @@ public class StrUtil { * StrUtil.subBetweenAll("#hello# world#!", "#"); = ["hello"] *
* - * @param str 被切割的字符串 + * @param str 被切割的字符串 * @param prefixAndSuffix 截取开始和结束的字符串标识 * @return 截取后的字符串 * @author gotanks @@ -2620,7 +2618,7 @@ public class StrUtil { * * @param str1 要比较的字符串1 * @param str2 要比较的字符串2 - * @return 如果两个字符串相同,或者都是null,则返回true + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} */ public static boolean equals(CharSequence str1, CharSequence str2) { return equals(str1, str2, false); @@ -2639,7 +2637,7 @@ public class StrUtil { * * @param str1 要比较的字符串1 * @param str2 要比较的字符串2 - * @return 如果两个字符串相同,或者都是null,则返回true + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} */ public static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) { return equals(str1, str2, true); @@ -2651,7 +2649,7 @@ public class StrUtil { * @param str1 要比较的字符串1 * @param str2 要比较的字符串2 * @param ignoreCase 是否忽略大小写 - * @return 如果两个字符串相同,或者都是null,则返回true + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} * @since 3.2.0 */ public static boolean equals(CharSequence str1, CharSequence str2, boolean ignoreCase) { @@ -3431,7 +3429,7 @@ public class StrUtil { * StrUtil.padAfter("123", 2, '0');//"23" *
* - * @param str 字符串,如果为null,直接返回null + * @param str 字符串,如果为{@code null},直接返回null * @param minLength 最小长度 * @param padChar 补充的字符 * @return 补充后的字符串 @@ -3459,7 +3457,7 @@ public class StrUtil { * StrUtil.padAfter("123", 2, "ABC");//"23" *
* - * @param str 字符串,如果为null,直接返回null + * @param str 字符串,如果为{@code null},直接返回null * @param minLength 最小长度 * @param padStr 补充的字符 * @return 补充后的字符串 diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java b/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java index e57a0f9ec..6d68a6bd4 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java @@ -1,49 +1,61 @@ package cn.hutool.dfa; -/** - * @author 肖海斌 - *

- * 匹配到的敏感词,包含敏感词,text中匹配敏感词的内容,以及匹配内容在text中的下标, - * 下标可以用来做敏感词的进一步处理,如果替换成** - */ -public class FoundWord { - /** - * 生效的敏感词 - */ - private String word; - /** - * 敏感词匹配到的内容 - */ - private String foundWord; - /** - * 匹配内容在待分析字符串中的开始位置 - */ - private int startIndex; - /** - * 匹配内容在待分析字符串中的结束位置 - */ - private int endIndex; +import cn.hutool.core.lang.DefaultSegment; - public FoundWord(String word, String foundWord, int start, int end) { +/** + *

+ * 匹配到的单词,包含单词,text中匹配单词的内容,以及匹配内容在text中的下标, + * 下标可以用来做单词的进一步处理,如果替换成** + * + * @author 肖海斌 + */ +public class FoundWord extends DefaultSegment { + /** + * 生效的单词,即单词树中的词 + */ + private final String word; + /** + * 单词匹配到的内容,即文中的单词 + */ + private final String foundWord; + + /** + * 构造 + * + * @param word 生效的单词,即单词树中的词 + * @param foundWord 单词匹配到的内容,即文中的单词 + * @param startIndex 起始位置(包含) + * @param endIndex 结束位置(包含) + */ + public FoundWord(String word, String foundWord, int startIndex, int endIndex) { + super(startIndex, endIndex); this.word = word; this.foundWord = foundWord; - this.startIndex = start; - this.endIndex = end; } + /** + * 获取生效的单词,即单词树中的词 + * + * @return 生效的单词 + */ public String getWord() { return word; } + /** + * 获取单词匹配到的内容,即文中的单词 + * @return 单词匹配到的内容 + */ public String getFoundWord() { return foundWord; } - public int getStartIndex() { - return startIndex; - } - - public int getEndIndex() { - return endIndex; + /** + * 默认的,只输出匹配到的关键字 + * @return 匹配到的关键字 + */ + @Override + public String toString() { + return this.foundWord; } } diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java index 6a396d49d..a7f0e05d5 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java @@ -1,6 +1,6 @@ package cn.hutool.dfa; -import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Filter; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.StrUtil; @@ -25,7 +25,7 @@ public final class SensitiveUtil { * @return 是否已经被初始化 */ public static boolean isInited() { - return !sensitiveTree.isEmpty(); + return false == sensitiveTree.isEmpty(); } /** @@ -117,19 +117,44 @@ public final class SensitiveUtil { * * @param text 文本 * @return 敏感词 + * @deprecated 请使用 {@link #getFoundFirstSensitive(String)} */ - public static FoundWord getFindedFirstSensitive(String text) { + @Deprecated + public static String getFindedFirstSensitive(String text) { return sensitiveTree.match(text); } + /** + * 查找敏感词,返回找到的第一个敏感词 + * + * @param text 文本 + * @return 敏感词 + * @since 5.5.3 + */ + public static FoundWord getFoundFirstSensitive(String text) { + return sensitiveTree.matchWord(text); + } + + /** + * 查找敏感词,返回找到的第一个敏感词 + * + * @param obj bean,会被转为JSON字符串 + * @return 敏感词 + * @deprecated 请使用 {@link #getFoundFirstSensitive(Object)} + */ + @Deprecated + public static String getFindedFirstSensitive(Object obj) { + return sensitiveTree.match(JSONUtil.toJsonStr(obj)); + } + /** * 查找敏感词,返回找到的第一个敏感词 * * @param obj bean,会被转为JSON字符串 * @return 敏感词 */ - public static FoundWord getFindedFirstSensitive(Object obj) { - return sensitiveTree.match(JSONUtil.toJsonStr(obj)); + public static FoundWord getFoundFirstSensitive(Object obj) { + return sensitiveTree.matchWord(JSONUtil.toJsonStr(obj)); } /** @@ -137,11 +162,40 @@ public final class SensitiveUtil { * * @param text 文本 * @return 敏感词 + * @deprecated 请使用 {@link #getFoundAllSensitive(String)} */ - public static List getFindedAllSensitive(String text) { + @Deprecated + public static List getFindedAllSensitive(String text) { return sensitiveTree.matchAll(text); } + /** + * 查找敏感词,返回找到的所有敏感词 + * + * @param text 文本 + * @return 敏感词 + * @since 5.5.3 + */ + public static List getFoundAllSensitive(String text) { + return sensitiveTree.matchAllWords(text); + } + + /** + * 查找敏感词,返回找到的所有敏感词
+ * 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
+ * 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * + * @param text 文本 + * @param isDensityMatch 是否使用密集匹配原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @return 敏感词 + * @deprecated 请使用 {@link #getFoundAllSensitive(String, boolean, boolean)} + */ + @Deprecated + public static List getFindedAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) { + return sensitiveTree.matchAll(text, -1, isDensityMatch, isGreedMatch); + } + /** * 查找敏感词,返回找到的所有敏感词
* 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
@@ -152,8 +206,8 @@ public final class SensitiveUtil { * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 * @return 敏感词 */ - public static List getFindedAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) { - return sensitiveTree.matchAll(text, -1, isDensityMatch, isGreedMatch); + public static List getFoundAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) { + return sensitiveTree.matchAllWords(text, -1, isDensityMatch, isGreedMatch); } /** @@ -161,11 +215,24 @@ public final class SensitiveUtil { * * @param bean 对象,会被转为JSON * @return 敏感词 + * @deprecated 请使用 {@link #getFoundAllSensitive(Object)} */ - public static List getFindedAllSensitive(Object bean) { + @Deprecated + public static List getFindedAllSensitive(Object bean) { return sensitiveTree.matchAll(JSONUtil.toJsonStr(bean)); } + /** + * 查找敏感词,返回找到的所有敏感词 + * + * @param bean 对象,会被转为JSON + * @return 敏感词 + * @since 5.5.3 + */ + public static List getFoundAllSensitive(Object bean) { + return sensitiveTree.matchAllWords(JSONUtil.toJsonStr(bean)); + } + /** * 查找敏感词,返回找到的所有敏感词
* 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
@@ -175,9 +242,26 @@ public final class SensitiveUtil { * @param isDensityMatch 是否使用密集匹配原则 * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 * @return 敏感词 + * @deprecated 请使用 {@link #getFoundAllSensitive(Object, boolean, boolean)} */ - public static List getFindedAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) { - return getFindedAllSensitive(JSONUtil.toJsonStr(bean), isDensityMatch, isGreedMatch); + @Deprecated + public static List getFindedAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) { + return sensitiveTree.matchAll(JSONUtil.toJsonStr(bean), -1, isDensityMatch, isGreedMatch); + } + + /** + * 查找敏感词,返回找到的所有敏感词
+ * 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
+ * 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * + * @param bean 对象,会被转为JSON + * @param isDensityMatch 是否使用密集匹配原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @return 敏感词 + * @since 5.5.3 + */ + public static List getFoundAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) { + return getFoundAllSensitive(JSONUtil.toJsonStr(bean), isDensityMatch, isGreedMatch); } /** @@ -191,23 +275,27 @@ public final class SensitiveUtil { */ public static T sensitiveFilter(T bean, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) { String jsonText = JSONUtil.toJsonStr(bean); - Class c = (Class) bean.getClass(); + @SuppressWarnings("unchecked") + final Class c = (Class) bean.getClass(); return JSONUtil.toBean(sensitiveFilter(jsonText, isGreedMatch, sensitiveProcessor), c); } /** + * 处理过滤文本中的敏感词,默认替换成* + * * @param text 文本 * @param isGreedMatch 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] * @param sensitiveProcessor 敏感词处理器,默认按匹配内容的字符数替换成* * @return 敏感词过滤处理后的文本 */ public static String sensitiveFilter(String text, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) { - if (null == text || text.trim().equals("")) { + if (StrUtil.isEmpty(text)) { return text; } + //敏感词过滤场景下,不需要密集匹配 - List foundWordList = getFindedAllSensitive(text, false, isGreedMatch); - if (CollectionUtil.isEmpty(foundWordList)) { + List foundWordList = getFoundAllSensitive(text, false, isGreedMatch); + if (CollUtil.isEmpty(foundWordList)) { return text; } sensitiveProcessor = sensitiveProcessor == null ? new SensitiveProcessor() { diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java b/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java index 4e05657b9..13e84f589 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java @@ -1,11 +1,17 @@ package cn.hutool.dfa; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Filter; import cn.hutool.core.text.StrBuilder; import cn.hutool.core.util.StrUtil; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * DFA(Deterministic Finite Automaton 确定有穷自动机) @@ -26,7 +32,7 @@ public class WordTree extends HashMap { private static final long serialVersionUID = -4646423269465809276L; /** - * 敏感词字符末尾标识,用于标识单词末尾字符 + * 单词字符末尾标识,用于标识单词末尾字符 */ private final Set endCharacterSet = new HashSet<>(); /** @@ -62,26 +68,30 @@ public class WordTree extends HashMap { * 增加一组单词 * * @param words 单词集合 + * @return this */ - public void addWords(Collection words) { + public WordTree addWords(Collection words) { if (false == (words instanceof Set)) { words = new HashSet<>(words); } for (String word : words) { addWord(word); } + return this; } /** * 增加一组单词 * * @param words 单词数组 + * @return this */ - public void addWords(String... words) { + public WordTree addWords(String... words) { HashSet wordsSet = CollectionUtil.newHashSet(words); for (String word : wordsSet) { addWord(word); } + return this; } /** @@ -89,7 +99,7 @@ public class WordTree extends HashMap { * * @param word 单词 */ - public void addWord(String word) { + public WordTree addWord(String word) { final Filter charFilter = this.charFilter; WordTree parent = null; WordTree current = this; @@ -112,8 +122,8 @@ public class WordTree extends HashMap { if (null != parent) { parent.setEnd(currentChar); } + return this; } - //------------------------------------------------------------------------------- match /** @@ -126,7 +136,7 @@ public class WordTree extends HashMap { if (null == text) { return false; } - return null != match(text); + return null != matchWord(text); } /** @@ -135,15 +145,24 @@ public class WordTree extends HashMap { * @param text 被检查的文本 * @return 匹配到的关键字 */ - public FoundWord match(String text) { + public String match(String text) { + final FoundWord foundWord = matchWord(text); + return null != foundWord ? foundWord.toString() : null; + } + + /** + * 获得第一个匹配的关键字 + * + * @param text 被检查的文本 + * @return 匹配到的关键字 + * @since 5.5.3 + */ + public FoundWord matchWord(String text) { if (null == text) { return null; } - List matchAll = matchAll(text, 1); - if (CollectionUtil.isNotEmpty(matchAll)) { - return matchAll.get(0); - } - return null; + final List matchAll = matchAllWords(text, 1); + return CollUtil.get(matchAll, 0); } //------------------------------------------------------------------------------- match all @@ -154,10 +173,21 @@ public class WordTree extends HashMap { * @param text 被检查的文本 * @return 匹配的词列表 */ - public List matchAll(String text) { + public List matchAll(String text) { return matchAll(text, -1); } + /** + * 找出所有匹配的关键字 + * + * @param text 被检查的文本 + * @return 匹配的词列表 + * @since 5.5.3 + */ + public List matchAllWords(String text) { + return matchAllWords(text, -1); + } + /** * 找出所有匹配的关键字 * @@ -165,10 +195,22 @@ public class WordTree extends HashMap { * @param limit 限制匹配个数 * @return 匹配的词列表 */ - public List matchAll(String text, int limit) { + public List matchAll(String text, int limit) { return matchAll(text, limit, false, false); } + /** + * 找出所有匹配的关键字 + * + * @param text 被检查的文本 + * @param limit 限制匹配个数 + * @return 匹配的词列表 + * @since 5.5.3 + */ + public List matchAllWords(String text, int limit) { + return matchAllWords(text, limit, false, false); + } + /** * 找出所有匹配的关键字
* 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
@@ -180,7 +222,24 @@ public class WordTree extends HashMap { * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 * @return 匹配的词列表 */ - public List matchAll(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) { + public List matchAll(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) { + final List matchAllWords = matchAllWords(text, limit, isDensityMatch, isGreedMatch); + return CollUtil.map(matchAllWords, FoundWord::toString, true); + } + + /** + * 找出所有匹配的关键字
+ * 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
+ * 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * + * @param text 被检查的文本 + * @param limit 限制匹配个数 + * @param isDensityMatch 是否使用密集匹配原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @return 匹配的词列表 + * @since 5.5.3 + */ + public List matchAllWords(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) { if (null == text) { return null; } @@ -239,8 +298,6 @@ public class WordTree extends HashMap { } return foundWords; } - - //--------------------------------------------------------------------------------------- Private method start /** diff --git a/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java b/hutool-dfa/src/test/java/cn/hutool/dfa/DfaTest.java similarity index 66% rename from hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java rename to hutool-dfa/src/test/java/cn/hutool/dfa/DfaTest.java index 913f10fce..344547476 100644 --- a/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java +++ b/hutool-dfa/src/test/java/cn/hutool/dfa/DfaTest.java @@ -1,13 +1,10 @@ -package cn.hutool.dfa.test; +package cn.hutool.dfa; -import cn.hutool.core.collection.CollectionUtil; -import cn.hutool.dfa.FoundWord; -import cn.hutool.dfa.WordTree; +import cn.hutool.core.collection.CollUtil; import org.junit.Assert; import org.junit.Test; import java.util.List; -import java.util.stream.Collectors; /** * DFA单元测试 @@ -29,8 +26,8 @@ public class DfaTest { // 情况一:标准匹配,匹配到最短关键词,并跳过已经匹配的关键词 // 匹配到【大】,就不再继续匹配了,因此【大土豆】不匹配 // 匹配到【刚出锅】,就跳过这三个字了,因此【出锅】不匹配(由于刚首先被匹配,因此长的被匹配,最短匹配只针对第一个字相同选最短) - List matchAll = tree.matchAll(text, -1, false, false); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅")); + List matchAll = tree.matchAll(text, -1, false, false); + Assert.assertEquals(matchAll, CollUtil.newArrayList("大", "土^豆", "刚出锅")); } /** @@ -45,8 +42,8 @@ public class DfaTest { // 情况二:匹配到最短关键词,不跳过已经匹配的关键词 // 【大】被匹配,最短匹配原则【大土豆】被跳过,【土豆继续被匹配】 // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 - List matchAll = tree.matchAll(text, -1, true, false); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅", "出锅")); + List matchAll = tree.matchAll(text, -1, true, false); + Assert.assertEquals(matchAll, CollUtil.newArrayList("大", "土^豆", "刚出锅", "出锅")); } /** @@ -61,8 +58,8 @@ public class DfaTest { // 情况三:匹配到最长关键词,跳过已经匹配的关键词 // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配 // 由于【大土豆】被匹配,【土豆】被跳过,由于【刚出锅】被匹配,【出锅】被跳过 - List matchAll = tree.matchAll(text, -1, false, true); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "大土^豆", "刚出锅")); + List matchAll = tree.matchAll(text, -1, false, true); + Assert.assertEquals(matchAll, CollUtil.newArrayList("大", "大土^豆", "刚出锅")); } @@ -78,8 +75,8 @@ public class DfaTest { // 情况四:匹配到最长关键词,不跳过已经匹配的关键词(最全关键词) // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配,由于不跳过已经匹配的关键词,土豆继续被匹配 // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 - List matchAll = tree.matchAll(text, -1, true, true); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "大土^豆", "土^豆", "刚出锅", "出锅")); + List matchAll = tree.matchAll(text, -1, true, true); + Assert.assertEquals(matchAll, CollUtil.newArrayList("大", "大土^豆", "土^豆", "刚出锅", "出锅")); } @@ -91,21 +88,20 @@ public class DfaTest { WordTree tree = new WordTree(); tree.addWord("tio"); - List all = tree.matchAll("AAAAAAAt-ioBBBBBBB"); - Assert.assertEquals(all.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("t-io")); + List all = tree.matchAll("AAAAAAAt-ioBBBBBBB"); + Assert.assertEquals(all, CollUtil.newArrayList("t-io")); } @Test - public void aTest() { + public void aTest(){ WordTree tree = new WordTree(); tree.addWord("women"); String text = "a WOMEN todo.".toLowerCase(); - List matchAll = tree.matchAll(text, -1, false, false); - Assert.assertEquals("[women]", matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()).toString()); + List matchAll = tree.matchAll(text, -1, false, false); + Assert.assertEquals("[women]", matchAll.toString()); } // ---------------------------------------------------------------------------------------------------------- - /** * 构建查找树 * @@ -121,4 +117,4 @@ public class DfaTest { tree.addWord("出锅"); return tree; } -} +} \ No newline at end of file diff --git a/hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java b/hutool-dfa/src/test/java/cn/hutool/dfa/SensitiveUtilTest.java similarity index 93% rename from hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java rename to hutool-dfa/src/test/java/cn/hutool/dfa/SensitiveUtilTest.java index 8a19fef3e..ba7348b09 100644 --- a/hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java +++ b/hutool-dfa/src/test/java/cn/hutool/dfa/SensitiveUtilTest.java @@ -1,6 +1,5 @@ -package cn.hutool.dfa.test; +package cn.hutool.dfa; -import cn.hutool.dfa.SensitiveUtil; import org.junit.Assert; import org.junit.Test; diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java b/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java index ad3948af2..3afa65cc2 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java @@ -694,12 +694,12 @@ public final class JSONUtil { * 在需要的时候包装对象
* 包装包括: *

    - *
  • null =》 JSONNull.NULL
  • + *
  • {@code null} =》 {@code JSONNull.NULL}
  • *
  • array or collection =》 JSONArray
  • *
  • map =》 JSONObject
  • *
  • standard property (Double, String, et al) =》 原对象
  • *
  • 来自于java包 =》 字符串
  • - *
  • 其它 =》 尝试包装为JSONObject,否则返回null
  • + *
  • 其它 =》 尝试包装为JSONObject,否则返回{@code null}
  • *
* * @param object 被包装的对象 From 7633733cfe65f53ef0be5d5ec8d6125791f3412d Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 8 Dec 2020 05:03:48 +0800 Subject: [PATCH 24/47] add Segment --- CHANGELOG.md | 1 + .../java/cn/hutool/core/util/PageUtil.java | 32 +++++++++++++++++++ .../src/main/java/cn/hutool/db/Page.java | 15 ++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97a428e88..332b8d634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * 【cache 】 CacheObj的isExpired()逻辑修改(issue#1295@Github) * 【json 】 JSONStrFormater改为JSONStrFormatter * 【dfa 】 增加FoundWord(pr#1290@Github) +* 【core 】 增加Segment(pr#1290@Github) ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/util/PageUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/PageUtil.java index 59670070d..d56e95234 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/PageUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/PageUtil.java @@ -1,5 +1,8 @@ package cn.hutool.core.util; +import cn.hutool.core.lang.DefaultSegment; +import cn.hutool.core.lang.Segment; + /** * 分页工具类 * @@ -135,6 +138,35 @@ public class PageUtil { return new int[]{start, getEndByStart(start, pageSize)}; } + /** + * 将页数和每页条目数转换为开始位置和结束位置
+ * 此方法用于包括结束位置的分页方法
+ * 例如: + * + *
+	 * 页码:0,每页10 =》 [0, 10]
+	 * 页码:1,每页10 =》 [10, 20]
+	 * ……
+	 * 
+ * + *

+ * 当{@link #setFirstPageNo(int)}设置为1时: + *

+	 * 页码:1,每页10 =》 [0, 10]
+	 * 页码:2,每页10 =》 [10, 20]
+	 * ……
+	 * 
+ * + * @param pageNo 页码(从0计数) + * @param pageSize 每页条目数 + * @return {@link Segment} + * @since 5.5.3 + */ + public static Segment toSegment(int pageNo, int pageSize) { + final int[] startEnd = transToStartEnd(pageNo, pageSize); + return new DefaultSegment<>(startEnd[0], startEnd[1]); + } + /** * 根据总数计算总页数 * diff --git a/hutool-db/src/main/java/cn/hutool/db/Page.java b/hutool-db/src/main/java/cn/hutool/db/Page.java index 8841ee4a3..c524584b8 100644 --- a/hutool-db/src/main/java/cn/hutool/db/Page.java +++ b/hutool-db/src/main/java/cn/hutool/db/Page.java @@ -1,5 +1,6 @@ package cn.hutool.db; +import cn.hutool.core.lang.Segment; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.PageUtil; import cn.hutool.db.sql.Order; @@ -12,7 +13,7 @@ import java.util.Arrays; * * @author Looly */ -public class Page implements Serializable { +public class Page implements Segment, Serializable { private static final long serialVersionUID = 97792549823353462L; public static final int DEFAULT_PAGE_SIZE = 20; @@ -159,15 +160,27 @@ public class Page implements Serializable { /** * @return 开始位置 + * @see #getStartIndex() */ public int getStartPosition() { + return getStartIndex(); + } + + @Override + public Integer getStartIndex() { return PageUtil.getStart(this.pageNumber, this.pageSize); } /** * @return 结束位置 + * @see #getEndIndex() */ public int getEndPosition() { + return getEndIndex(); + } + + @Override + public Integer getEndIndex() { return PageUtil.getEnd(this.pageNumber, this.pageSize); } From 7a03bbe93a57929761296eee56fd5e7a88561916 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 8 Dec 2020 06:15:13 +0800 Subject: [PATCH 25/47] add CharSequenceUtil --- CHANGELOG.md | 1 + hutool-core/pom.xml | 8 + .../cn/hutool/core/text/CharSequenceUtil.java | 4231 ++++++++++++++++ .../java/cn/hutool/core/util/StrUtil.java | 4237 +---------------- .../java/cn/hutool/core/util/XmlUtil.java | 30 + .../cn/hutool/extra/spring/package-info.java | 7 + ...tilTest.java => EnableSpringUtilTest.java} | 16 +- 7 files changed, 4325 insertions(+), 4205 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/CharSequenceUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/spring/package-info.java rename hutool-extra/src/test/java/cn/hutool/extra/spring/{EnableSprintUtilTest.java => EnableSpringUtilTest.java} (56%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 332b8d634..65dd28373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * 【json 】 JSONStrFormater改为JSONStrFormatter * 【dfa 】 增加FoundWord(pr#1290@Github) * 【core 】 增加Segment(pr#1290@Github) +* 【core 】 增加CharSequenceUtil ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-core/pom.xml b/hutool-core/pom.xml index d2d3282fe..3680e13a0 100644 --- a/hutool-core/pom.xml +++ b/hutool-core/pom.xml @@ -5,6 +5,14 @@ 4.0.0 jar + + + org.apache.poi + poi + 4.1.2 + compile + + cn.hutool diff --git a/hutool-core/src/main/java/cn/hutool/core/text/CharSequenceUtil.java b/hutool-core/src/main/java/cn/hutool/core/text/CharSequenceUtil.java new file mode 100644 index 000000000..96ec94cfe --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/CharSequenceUtil.java @@ -0,0 +1,4231 @@ +package cn.hutool.core.text; + +import cn.hutool.core.comparator.VersionComparator; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * {@link CharSequence} 相关工具类封装 + * + * @author looly + * @since 5.5.3 + */ +public class CharSequenceUtil { + + public static final int INDEX_NOT_FOUND = -1; + + /** + * 字符串常量:{@code "null"}
+ * 注意:{@code "null" != null} + */ + public static final String NULL = "null"; + + /** + * 字符串常量:空字符串 {@code ""} + */ + public static final String EMPTY = ""; + + /** + * 字符串常量:空格符 {@code " "} + */ + public static final String SPACE = " "; + + /** + *

字符串是否为空白,空白的定义如下:

+ *
    + *
  1. {@code null}
  2. + *
  3. 空字符串:{@code ""}
  4. + *
  5. 空格、全角空格、制表符、换行符,等不可见字符
  6. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isBlank(null) // true}
  • + *
  • {@code StrUtil.isBlank("") // true}
  • + *
  • {@code StrUtil.isBlank(" \t\n") // true}
  • + *
  • {@code StrUtil.isBlank("abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #isEmpty(CharSequence)} 的区别是: + * 该方法会校验空白字符,且性能相对于 {@link #isEmpty(CharSequence)} 略慢。

+ *
+ * + *

建议:

+ *
    + *
  • 该方法建议仅对于客户端(或第三方接口)传入的参数使用该方法。
  • + *
  • 需要同时校验多个字符串时,建议采用 {@link #hasBlank(CharSequence...)} 或 {@link #isAllBlank(CharSequence...)}
  • + *
+ * + * @param str 被检测的字符串 + * @return 若为空白,则返回 true + * @see #isEmpty(CharSequence) + */ + public static boolean isBlank(CharSequence str) { + int length; + + if ((str == null) || ((length = str.length()) == 0)) { + return true; + } + + for (int i = 0; i < length; i++) { + // 只要有一个非空字符即为非空字符串 + if (false == CharUtil.isBlankChar(str.charAt(i))) { + return false; + } + } + + return true; + } + + /** + *

字符串是否为非空白,非空白的定义如下:

+ *
    + *
  1. 不为 {@code null}
  2. + *
  3. 不为空字符串:{@code ""}
  4. + *
  5. 不为空格、全角空格、制表符、换行符,等不可见字符
  6. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isNotBlank(null) // false}
  • + *
  • {@code StrUtil.isNotBlank("") // false}
  • + *
  • {@code StrUtil.isNotBlank(" \t\n") // false}
  • + *
  • {@code StrUtil.isNotBlank("abc") // true}
  • + *
+ * + *

注意:该方法与 {@link #isNotEmpty(CharSequence)} 的区别是: + * 该方法会校验空白字符,且性能相对于 {@link #isNotEmpty(CharSequence)} 略慢。

+ *

建议:仅对于客户端(或第三方接口)传入的参数使用该方法。

+ * + * @param str 被检测的字符串 + * @return 是否为非空 + * @see #isBlank(CharSequence) + */ + public static boolean isNotBlank(CharSequence str) { + return false == isBlank(str); + } + + /** + *

指定字符串数组中,是否包含空字符串。

+ *

如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.hasBlank() // true}
  • + *
  • {@code StrUtil.hasBlank("", null, " ") // true}
  • + *
  • {@code StrUtil.hasBlank("123", " ") // true}
  • + *
  • {@code StrUtil.hasBlank("123", "abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #isAllBlank(CharSequence...)} 的区别在于:

+ *
    + *
  • hasBlank(CharSequence...) 等价于 {@code isBlank(...) || isBlank(...) || ...}
  • + *
  • {@link #isAllBlank(CharSequence...)} 等价于 {@code isBlank(...) && isBlank(...) && ...}
  • + *
+ * + * @param strs 字符串列表 + * @return 是否包含空字符串 + */ + public static boolean hasBlank(CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return true; + } + + for (CharSequence str : strs) { + if (isBlank(str)) { + return true; + } + } + return false; + } + + /** + *

指定字符串数组中的元素,是否全部为空字符串。

+ *

如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isAllBlank() // true}
  • + *
  • {@code StrUtil.isAllBlank("", null, " ") // true}
  • + *
  • {@code StrUtil.isAllBlank("123", " ") // false}
  • + *
  • {@code StrUtil.isAllBlank("123", "abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #hasBlank(CharSequence...)} 的区别在于:

+ *
    + *
  • {@link #hasBlank(CharSequence...)} 等价于 {@code isBlank(...) || isBlank(...) || ...}
  • + *
  • isAllBlank(CharSequence...) 等价于 {@code isBlank(...) && isBlank(...) && ...}
  • + *
+ * + * @param strs 字符串列表 + * @return 所有字符串是否为空白 + */ + public static boolean isAllBlank(CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return true; + } + + for (CharSequence str : strs) { + if (isNotBlank(str)) { + return false; + } + } + return true; + } + + /** + *

字符串是否为空,空的定义如下:

+ *
    + *
  1. {@code null}
  2. + *
  3. 空字符串:{@code ""}
  4. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isEmpty(null) // true}
  • + *
  • {@code StrUtil.isEmpty("") // true}
  • + *
  • {@code StrUtil.isEmpty(" \t\n") // false}
  • + *
  • {@code StrUtil.isEmpty("abc") // false}
  • + *
+ * + *

注意:该方法与 {@link #isBlank(CharSequence)} 的区别是:该方法不校验空白字符。

+ *

建议:

+ *
    + *
  • 该方法建议用于工具类或任何可以预期的方法参数的校验中。
  • + *
  • 需要同时校验多个字符串时,建议采用 {@link #hasEmpty(CharSequence...)} 或 {@link #isAllEmpty(CharSequence...)}
  • + *
+ * + * @param str 被检测的字符串 + * @return 是否为空 + * @see #isBlank(CharSequence) + */ + public static boolean isEmpty(CharSequence str) { + return str == null || str.length() == 0; + } + + /** + *

字符串是否为非空白,非空白的定义如下:

+ *
    + *
  1. 不为 {@code null}
  2. + *
  3. 不为空字符串:{@code ""}
  4. + *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isNotEmpty(null) // false}
  • + *
  • {@code StrUtil.isNotEmpty("") // false}
  • + *
  • {@code StrUtil.isNotEmpty(" \t\n") // true}
  • + *
  • {@code StrUtil.isNotEmpty("abc") // true}
  • + *
+ * + *

注意:该方法与 {@link #isNotBlank(CharSequence)} 的区别是:该方法不校验空白字符。

+ *

建议:该方法建议用于工具类或任何可以预期的方法参数的校验中。

+ * + * @param str 被检测的字符串 + * @return 是否为非空 + * @see #isEmpty(CharSequence) + */ + public static boolean isNotEmpty(CharSequence str) { + return false == isEmpty(str); + } + + /** + * 当给定字符串为null时,转换为Empty + * + * @param str 被检查的字符串 + * @return 原字符串或者空串 + * @see #nullToEmpty(CharSequence) + * @since 4.6.3 + */ + public static String emptyIfNull(CharSequence str) { + return nullToEmpty(str); + } + + /** + * 当给定字符串为null时,转换为Empty + * + * @param str 被转换的字符串 + * @return 转换后的字符串 + */ + public static String nullToEmpty(CharSequence str) { + return nullToDefault(str, EMPTY); + } + + /** + * 如果字符串是 {@code null},则返回指定默认字符串,否则返回字符串本身。 + * + *
+	 * nullToDefault(null, "default")  = "default"
+	 * nullToDefault("", "default")    = ""
+	 * nullToDefault("  ", "default")  = "  "
+	 * nullToDefault("bat", "default") = "bat"
+	 * 
+ * + * @param str 要转换的字符串 + * @param defaultStr 默认字符串 + * @return 字符串本身或指定的默认字符串 + */ + public static String nullToDefault(CharSequence str, String defaultStr) { + return (str == null) ? defaultStr : str.toString(); + } + + /** + * 如果字符串是{@code null}或者"",则返回指定默认字符串,否则返回字符串本身。 + * + *
+	 * emptyToDefault(null, "default")  = "default"
+	 * emptyToDefault("", "default")    = "default"
+	 * emptyToDefault("  ", "default")  = "  "
+	 * emptyToDefault("bat", "default") = "bat"
+	 * 
+ * + * @param str 要转换的字符串 + * @param defaultStr 默认字符串 + * @return 字符串本身或指定的默认字符串 + * @since 4.1.0 + */ + public static String emptyToDefault(CharSequence str, String defaultStr) { + return isEmpty(str) ? defaultStr : str.toString(); + } + + /** + * 如果字符串是{@code null}或者""或者空白,则返回指定默认字符串,否则返回字符串本身。 + * + *
+	 * emptyToDefault(null, "default")  = "default"
+	 * emptyToDefault("", "default")    = "default"
+	 * emptyToDefault("  ", "default")  = "default"
+	 * emptyToDefault("bat", "default") = "bat"
+	 * 
+ * + * @param str 要转换的字符串 + * @param defaultStr 默认字符串 + * @return 字符串本身或指定的默认字符串 + * @since 4.1.0 + */ + public static String blankToDefault(CharSequence str, String defaultStr) { + return isBlank(str) ? defaultStr : str.toString(); + } + + /** + * 当给定字符串为空字符串时,转换为{@code null} + * + * @param str 被转换的字符串 + * @return 转换后的字符串 + */ + public static String emptyToNull(CharSequence str) { + return isEmpty(str) ? null : str.toString(); + } + + /** + *

是否包含空字符串。

+ *

如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.hasEmpty() // true}
  • + *
  • {@code StrUtil.hasEmpty("", null) // true}
  • + *
  • {@code StrUtil.hasEmpty("123", "") // true}
  • + *
  • {@code StrUtil.hasEmpty("123", "abc") // false}
  • + *
  • {@code StrUtil.hasEmpty(" ", "\t", "\n") // false}
  • + *
+ * + *

注意:该方法与 {@link #isAllEmpty(CharSequence...)} 的区别在于:

+ *
    + *
  • hasEmpty(CharSequence...) 等价于 {@code isEmpty(...) || isEmpty(...) || ...}
  • + *
  • {@link #isAllEmpty(CharSequence...)} 等价于 {@code isEmpty(...) && isEmpty(...) && ...}
  • + *
+ * + * @param strs 字符串列表 + * @return 是否包含空字符串 + */ + public static boolean hasEmpty(CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return true; + } + + for (CharSequence str : strs) { + if (isEmpty(str)) { + return true; + } + } + return false; + } + + /** + *

指定字符串数组中的元素,是否全部为空字符串。

+ *

如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isAllEmpty() // true}
  • + *
  • {@code StrUtil.isAllEmpty("", null) // true}
  • + *
  • {@code StrUtil.isAllEmpty("123", "") // false}
  • + *
  • {@code StrUtil.isAllEmpty("123", "abc") // false}
  • + *
  • {@code StrUtil.isAllEmpty(" ", "\t", "\n") // false}
  • + *
+ * + *

注意:该方法与 {@link #hasEmpty(CharSequence...)} 的区别在于:

+ *
    + *
  • {@link #hasEmpty(CharSequence...)} 等价于 {@code isEmpty(...) || isEmpty(...) || ...}
  • + *
  • isAllEmpty(CharSequence...) 等价于 {@code isEmpty(...) && isEmpty(...) && ...}
  • + *
+ * + * @param strs 字符串列表 + * @return 所有字符串是否为空白 + */ + public static boolean isAllEmpty(CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return true; + } + + for (CharSequence str : strs) { + if (isNotEmpty(str)) { + return false; + } + } + return true; + } + + /** + *

指定字符串数组中的元素,是否都不为空字符串。

+ *

如果指定的字符串数组的长度不为 0,或者所有元素都不是空字符串,则返回 true。

+ *
+ * + *

例:

+ *
    + *
  • {@code StrUtil.isAllNotEmpty() // false}
  • + *
  • {@code StrUtil.isAllNotEmpty("", null) // false}
  • + *
  • {@code StrUtil.isAllNotEmpty("123", "") // false}
  • + *
  • {@code StrUtil.isAllNotEmpty("123", "abc") // true}
  • + *
  • {@code StrUtil.isAllNotEmpty(" ", "\t", "\n") // true}
  • + *
+ * + *

注意:该方法与 {@link #isAllEmpty(CharSequence...)} 的区别在于:

+ *
    + *
  • {@link #isAllEmpty(CharSequence...)} 等价于 {@code isEmpty(...) && isEmpty(...) && ...}
  • + *
  • isAllNotEmpty(CharSequence...) 等价于 {@code !isEmpty(...) && !isEmpty(...) && ...}
  • + *
+ * + * @param args 字符串数组 + * @return 所有字符串是否都不为为空白 + * @since 5.3.6 + */ + public static boolean isAllNotEmpty(CharSequence... args) { + return false == hasEmpty(args); + } + + /** + * 是否存都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素 + * + * @param args 被检查的对象,一个或者多个 + * @return 是否都不为空 + * @since 5.3.6 + */ + public static boolean isAllNotBlank(CharSequence... args) { + return false == hasBlank(args); + } + + /** + * 检查字符串是否为null、“null”、“undefined” + * + * @param str 被检查的字符串 + * @return 是否为null、“null”、“undefined” + * @since 4.0.10 + */ + public static boolean isNullOrUndefined(CharSequence str) { + if (null == str) { + return true; + } + return isNullOrUndefinedStr(str); + } + + /** + * 检查字符串是否为null、“”、“null”、“undefined” + * + * @param str 被检查的字符串 + * @return 是否为null、“”、“null”、“undefined” + * @since 4.0.10 + */ + public static boolean isEmptyOrUndefined(CharSequence str) { + if (isEmpty(str)) { + return true; + } + return isNullOrUndefinedStr(str); + } + + /** + * 检查字符串是否为null、空白串、“null”、“undefined” + * + * @param str 被检查的字符串 + * @return 是否为null、空白串、“null”、“undefined” + * @since 4.0.10 + */ + public static boolean isBlankOrUndefined(CharSequence str) { + if (isBlank(str)) { + return true; + } + return isNullOrUndefinedStr(str); + } + + /** + * 是否为“null”、“undefined”,不做空指针检查 + * + * @param str 字符串 + * @return 是否为“null”、“undefined” + */ + private static boolean isNullOrUndefinedStr(CharSequence str) { + String strString = str.toString().trim(); + return NULL.equals(strString) || "undefined".equals(strString); + } + + // ------------------------------------------------------------------------ Trim + + /** + * 除去字符串头尾部的空白,如果字符串是{@code null},依然返回{@code null}。 + * + *

+ * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * + *

+	 * trim(null)          = null
+	 * trim("")            = ""
+	 * trim("     ")       = ""
+	 * trim("abc")         = "abc"
+	 * trim("    abc    ") = "abc"
+	 * 
+ * + * @param str 要处理的字符串 + * @return 除去头尾空白的字符串,如果原字串为{@code null},则返回{@code null} + */ + public static String trim(CharSequence str) { + return (null == str) ? null : trim(str, 0); + } + + /** + * 除去字符串头尾部的空白,如果字符串是{@code null},返回{@code ""}。 + * + *
+	 * StrUtil.trimToEmpty(null)          = ""
+	 * StrUtil.trimToEmpty("")            = ""
+	 * StrUtil.trimToEmpty("     ")       = ""
+	 * StrUtil.trimToEmpty("abc")         = "abc"
+	 * StrUtil.trimToEmpty("    abc    ") = "abc"
+	 * 
+ * + * @param str 字符串 + * @return 去除两边空白符后的字符串, 如果为null返回"" + * @since 3.1.1 + */ + public static String trimToEmpty(CharSequence str) { + return str == null ? EMPTY : trim(str); + } + + /** + * 除去字符串头尾部的空白,如果字符串是{@code null}或者"",返回{@code null}。 + * + *
+	 * StrUtil.trimToNull(null)          = null
+	 * StrUtil.trimToNull("")            = null
+	 * StrUtil.trimToNull("     ")       = null
+	 * StrUtil.trimToNull("abc")         = "abc"
+	 * StrUtil.trimToEmpty("    abc    ") = "abc"
+	 * 
+ * + * @param str 字符串 + * @return 去除两边空白符后的字符串, 如果为空返回null + * @since 3.2.1 + */ + public static String trimToNull(CharSequence str) { + final String trimStr = trim(str); + return EMPTY.equals(trimStr) ? null : trimStr; + } + + /** + * 除去字符串头部的空白,如果字符串是{@code null},则返回{@code null}。 + * + *

+ * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * + *

+	 * trimStart(null)         = null
+	 * trimStart("")           = ""
+	 * trimStart("abc")        = "abc"
+	 * trimStart("  abc")      = "abc"
+	 * trimStart("abc  ")      = "abc  "
+	 * trimStart(" abc ")      = "abc "
+	 * 
+ * + * @param str 要处理的字符串 + * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} + */ + public static String trimStart(CharSequence str) { + return trim(str, -1); + } + + /** + * 除去字符串尾部的空白,如果字符串是{@code null},则返回{@code null}。 + * + *

+ * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * + *

+	 * trimEnd(null)       = null
+	 * trimEnd("")         = ""
+	 * trimEnd("abc")      = "abc"
+	 * trimEnd("  abc")    = "  abc"
+	 * trimEnd("abc  ")    = "abc"
+	 * trimEnd(" abc ")    = " abc"
+	 * 
+ * + * @param str 要处理的字符串 + * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} + */ + public static String trimEnd(CharSequence str) { + return trim(str, 1); + } + + /** + * 除去字符串头尾部的空白符,如果字符串是{@code null},依然返回{@code null}。 + * + * @param str 要处理的字符串 + * @param mode {@code -1}表示trimStart,{@code 0}表示trim全部, {@code 1}表示trimEnd + * @return 除去指定字符后的的字符串,如果原字串为{@code null},则返回{@code null} + */ + public static String trim(CharSequence str, int mode) { + String result; + if (str == null) { + result = null; + } else { + int length = str.length(); + int start = 0; + int end = length;// 扫描字符串头部 + if (mode <= 0) { + while ((start < end) && (CharUtil.isBlankChar(str.charAt(start)))) { + start++; + } + }// 扫描字符串尾部 + if (mode >= 0) { + while ((start < end) && (CharUtil.isBlankChar(str.charAt(end - 1)))) { + end--; + } + } + if ((start > 0) || (end < length)) { + result = str.toString().substring(start, end); + } else { + result = str.toString(); + } + } + + return result; + } + + // ------------------------------------------------------------------------ startWith + + /** + * 字符串是否以给定字符开始 + * + * @param str 字符串 + * @param c 字符 + * @return 是否开始 + */ + public static boolean startWith(CharSequence str, char c) { + if (isEmpty(str)) { + return false; + } + return c == str.charAt(0); + } + + /** + * 是否以指定字符串开头
+ * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @param ignoreCase 是否忽略大小写 + * @return 是否以指定字符串开头 + * @since 5.4.3 + */ + public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase) { + return startWith(str, prefix, ignoreCase, false); + } + + /** + * 是否以指定字符串开头
+ * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @param ignoreCase 是否忽略大小写 + * @param ignoreEquals 是否忽略字符串相等的情况 + * @return 是否以指定字符串开头 + * @since 5.4.3 + */ + public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase, boolean ignoreEquals) { + if (null == str || null == prefix) { + if (false == ignoreEquals) { + return false; + } + return null == str && null == prefix; + } + + boolean isStartWith; + if (ignoreCase) { + isStartWith = str.toString().toLowerCase().startsWith(prefix.toString().toLowerCase()); + } else { + isStartWith = str.toString().startsWith(prefix.toString()); + } + + if (isStartWith) { + return (false == ignoreEquals) || (false == equals(str, prefix, ignoreCase)); + } + return false; + } + + /** + * 是否以指定字符串开头 + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @return 是否以指定字符串开头 + */ + public static boolean startWith(CharSequence str, CharSequence prefix) { + return startWith(str, prefix, false); + } + + /** + * 是否以指定字符串开头,忽略相等字符串的情况 + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @return 是否以指定字符串开头并且两个字符串不相等 + */ + public static boolean startWithIgnoreEquals(CharSequence str, CharSequence prefix) { + return startWith(str, prefix, false, true); + } + + /** + * 是否以指定字符串开头,忽略大小写 + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @return 是否以指定字符串开头 + */ + public static boolean startWithIgnoreCase(CharSequence str, CharSequence prefix) { + return startWith(str, prefix, true); + } + + /** + * 给定字符串是否以任何一个字符串开始
+ * 给定字符串和数组为空都返回false + * + * @param str 给定字符串 + * @param prefixes 需要检测的开始字符串 + * @return 给定字符串是否以任何一个字符串开始 + * @since 3.0.6 + */ + public static boolean startWithAny(CharSequence str, CharSequence... prefixes) { + if (isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { + return false; + } + + for (CharSequence suffix : prefixes) { + if (startWith(str, suffix, false)) { + return true; + } + } + return false; + } + + // ------------------------------------------------------------------------ endWith + + /** + * 字符串是否以给定字符结尾 + * + * @param str 字符串 + * @param c 字符 + * @return 是否结尾 + */ + public static boolean endWith(CharSequence str, char c) { + if (isEmpty(str)) { + return false; + } + return c == str.charAt(str.length() - 1); + } + + /** + * 是否以指定字符串结尾
+ * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false + * + * @param str 被监测字符串 + * @param suffix 结尾字符串 + * @param isIgnoreCase 是否忽略大小写 + * @return 是否以指定字符串结尾 + */ + public static boolean endWith(CharSequence str, CharSequence suffix, boolean isIgnoreCase) { + if (null == str || null == suffix) { + return null == str && null == suffix; + } + + if (isIgnoreCase) { + return str.toString().toLowerCase().endsWith(suffix.toString().toLowerCase()); + } else { + return str.toString().endsWith(suffix.toString()); + } + } + + /** + * 是否以指定字符串结尾 + * + * @param str 被监测字符串 + * @param suffix 结尾字符串 + * @return 是否以指定字符串结尾 + */ + public static boolean endWith(CharSequence str, CharSequence suffix) { + return endWith(str, suffix, false); + } + + /** + * 是否以指定字符串结尾,忽略大小写 + * + * @param str 被监测字符串 + * @param suffix 结尾字符串 + * @return 是否以指定字符串结尾 + */ + public static boolean endWithIgnoreCase(CharSequence str, CharSequence suffix) { + return endWith(str, suffix, true); + } + + /** + * 给定字符串是否以任何一个字符串结尾
+ * 给定字符串和数组为空都返回false + * + * @param str 给定字符串 + * @param suffixes 需要检测的结尾字符串 + * @return 给定字符串是否以任何一个字符串结尾 + * @since 3.0.6 + */ + public static boolean endWithAny(CharSequence str, CharSequence... suffixes) { + if (isEmpty(str) || ArrayUtil.isEmpty(suffixes)) { + return false; + } + + for (CharSequence suffix : suffixes) { + if (endWith(str, suffix, false)) { + return true; + } + } + return false; + } + + // ------------------------------------------------------------------------ contains + + /** + * 指定字符是否在字符串中出现过 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @return 是否包含 + * @since 3.1.2 + */ + public static boolean contains(CharSequence str, char searchChar) { + return indexOf(str, searchChar) > -1; + } + + /** + * 指定字符串是否在字符串中出现过 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @return 是否包含 + * @since 5.1.1 + */ + public static boolean contains(CharSequence str, CharSequence searchStr) { + if (null == str || null == searchStr) { + return false; + } + return str.toString().contains(searchStr); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + * @since 3.2.0 + */ + public static boolean containsAny(CharSequence str, CharSequence... testStrs) { + return null != getContainsStr(str, testStrs); + } + + /** + * 查找指定字符串是否包含指定字符列表中的任意一个字符 + * + * @param str 指定字符串 + * @param testChars 需要检查的字符数组 + * @return 是否包含任意一个字符 + * @since 4.1.11 + */ + public static boolean containsAny(CharSequence str, char... testChars) { + if (false == isEmpty(str)) { + int len = str.length(); + for (int i = 0; i < len; i++) { + if (ArrayUtil.contains(testChars, str.charAt(i))) { + return true; + } + } + } + return false; + } + + /** + * 检查指定字符串中是否只包含给定的字符 + * + * @param str 字符串 + * @param testChars 检查的字符 + * @return 字符串含有非检查的字符,返回false + * @since 4.4.1 + */ + public static boolean containsOnly(CharSequence str, char... testChars) { + if (false == isEmpty(str)) { + int len = str.length(); + for (int i = 0; i < len; i++) { + if (false == ArrayUtil.contains(testChars, str.charAt(i))) { + return false; + } + } + } + return true; + } + + /** + * 给定字符串是否包含空白符(空白符包括空格、制表符、全角空格和不间断空格)
+ * 如果给定字符串为null或者"",则返回false + * + * @param str 字符串 + * @return 是否包含空白符 + * @since 4.0.8 + */ + public static boolean containsBlank(CharSequence str) { + if (null == str) { + return false; + } + final int length = str.length(); + if (0 == length) { + return false; + } + + for (int i = 0; i < length; i += 1) { + if (CharUtil.isBlankChar(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 被包含的第一个字符串 + * @since 3.2.0 + */ + public static String getContainsStr(CharSequence str, CharSequence... testStrs) { + if (isEmpty(str) || ArrayUtil.isEmpty(testStrs)) { + return null; + } + for (CharSequence checkStr : testStrs) { + if (str.toString().contains(checkStr)) { + return checkStr.toString(); + } + } + return null; + } + + /** + * 是否包含特定字符,忽略大小写,如果给定两个参数都为{@code null},返回true + * + * @param str 被检测字符串 + * @param testStr 被测试是否包含的字符串 + * @return 是否包含 + */ + public static boolean containsIgnoreCase(CharSequence str, CharSequence testStr) { + if (null == str) { + // 如果被监测字符串和 + return null == testStr; + } + return str.toString().toLowerCase().contains(testStr.toString().toLowerCase()); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串
+ * 忽略大小写 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + * @since 3.2.0 + */ + public static boolean containsAnyIgnoreCase(CharSequence str, CharSequence... testStrs) { + return null != getContainsStrIgnoreCase(str, testStrs); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串
+ * 忽略大小写 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 被包含的第一个字符串 + * @since 3.2.0 + */ + public static String getContainsStrIgnoreCase(CharSequence str, CharSequence... testStrs) { + if (isEmpty(str) || ArrayUtil.isEmpty(testStrs)) { + return null; + } + for (CharSequence testStr : testStrs) { + if (containsIgnoreCase(str, testStr)) { + return testStr.toString(); + } + } + return null; + } + + // ------------------------------------------------------------------------ indexOf + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar) { + return indexOf(str, searchChar, 0); + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @return 位置 + */ + public static int indexOf(CharSequence str, char searchChar, int start) { + if (str instanceof String) { + return ((String) str).indexOf(searchChar, start); + } else { + return indexOf(str, searchChar, start, -1); + } + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @param end 终止位置,如果超过str.length()则默认查找到字符串末尾 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar, int start, int end) { + if (isEmpty(str)) { + return INDEX_NOT_FOUND; + } + final int len = str.length(); + if (start < 0 || start > len) { + start = 0; + } + if (end > len || end < 0) { + end = len; + } + for (int i = start; i < end; i++) { + if (str.charAt(i) == searchChar) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * + *
+	 * StrUtil.indexOfIgnoreCase(null, *, *)          = -1
+	 * StrUtil.indexOfIgnoreCase(*, null, *)          = -1
+	 * StrUtil.indexOfIgnoreCase("", "", 0)           = 0
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+	 * StrUtil.indexOfIgnoreCase("abc", "", 9)        = -1
+	 * 
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + return indexOfIgnoreCase(str, searchStr, 0); + } + + /** + * 指定范围内查找字符串 + * + *
+	 * StrUtil.indexOfIgnoreCase(null, *, *)          = -1
+	 * StrUtil.indexOfIgnoreCase(*, null, *)          = -1
+	 * StrUtil.indexOfIgnoreCase("", "", 0)           = 0
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+	 * StrUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+	 * StrUtil.indexOfIgnoreCase("abc", "", 9)        = -1
+	 * 
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { + return indexOf(str, searchStr, fromIndex, true); + } + + /** + * 指定范围内查找字符串 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置 + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOf(final CharSequence str, CharSequence searchStr, int fromIndex, boolean ignoreCase) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (fromIndex < 0) { + fromIndex = 0; + } + + final int endLimit = str.length() - searchStr.length() + 1; + if (fromIndex > endLimit) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return fromIndex; + } + + if (false == ignoreCase) { + // 不忽略大小写调用JDK方法 + return str.toString().indexOf(searchStr.toString(), fromIndex); + } + + for (int i = fromIndex; i < endLimit; i++) { + if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + * 指定范围内查找字符串,忽略大小写 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + return lastIndexOfIgnoreCase(str, searchStr, str.length()); + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * fromIndex 为搜索起始位置,从后往前计数 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置,从后往前计数 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { + return lastIndexOf(str, searchStr, fromIndex, true); + } + + /** + * 指定范围内查找字符串
+ * fromIndex 为搜索起始位置,从后往前计数 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置,从后往前计数 + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOf(final CharSequence str, final CharSequence searchStr, int fromIndex, boolean ignoreCase) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (fromIndex < 0) { + fromIndex = 0; + } + fromIndex = Math.min(fromIndex, str.length()); + + if (searchStr.length() == 0) { + return fromIndex; + } + + if (false == ignoreCase) { + // 不忽略大小写调用JDK方法 + return str.toString().lastIndexOf(searchStr.toString(), fromIndex); + } + + for (int i = fromIndex; i >= 0; i--) { + if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回字符串 searchStr 在字符串 str 中第 ordinal 次出现的位置。 + * + *

+ * 如果 str=null 或 searchStr=null 或 ordinal≥0 则返回-1
+ * 此方法来自:Apache-Commons-Lang + *

+ * 例子(*代表任意字符): + * + *

+	 * StrUtil.ordinalIndexOf(null, *, *)          = -1
+	 * StrUtil.ordinalIndexOf(*, null, *)          = -1
+	 * StrUtil.ordinalIndexOf("", "", *)           = 0
+	 * StrUtil.ordinalIndexOf("aabaabaa", "a", 1)  = 0
+	 * StrUtil.ordinalIndexOf("aabaabaa", "a", 2)  = 1
+	 * StrUtil.ordinalIndexOf("aabaabaa", "b", 1)  = 2
+	 * StrUtil.ordinalIndexOf("aabaabaa", "b", 2)  = 5
+	 * StrUtil.ordinalIndexOf("aabaabaa", "ab", 1) = 1
+	 * StrUtil.ordinalIndexOf("aabaabaa", "ab", 2) = 4
+	 * StrUtil.ordinalIndexOf("aabaabaa", "", 1)   = 0
+	 * StrUtil.ordinalIndexOf("aabaabaa", "", 2)   = 0
+	 * 
+ * + * @param str 被检查的字符串,可以为null + * @param searchStr 被查找的字符串,可以为null + * @param ordinal 第几次出现的位置 + * @return 查找到的位置 + * @since 3.2.3 + */ + public static int ordinalIndexOf(CharSequence str, CharSequence searchStr, int ordinal) { + if (str == null || searchStr == null || ordinal <= 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return 0; + } + int found = 0; + int index = INDEX_NOT_FOUND; + do { + index = indexOf(str, searchStr, index + 1, false); + if (index < 0) { + return index; + } + found++; + } while (found < ordinal); + return index; + } + + // ------------------------------------------------------------------------ remove + + /** + * 移除字符串中所有给定字符串
+ * 例:removeAll("aa-bb-cc-dd", "-") =》 aabbccdd + * + * @param str 字符串 + * @param strToRemove 被移除的字符串 + * @return 移除后的字符串 + */ + public static String removeAll(CharSequence str, CharSequence strToRemove) { + // strToRemove如果为空, 也不用继续后面的逻辑 + if (isEmpty(str) || isEmpty(strToRemove)) { + return str(str); + } + return str.toString().replace(strToRemove, EMPTY); + } + + /** + * 移除字符串中所有给定字符串,当某个字符串出现多次,则全部移除
+ * 例:removeAny("aa-bb-cc-dd", "a", "b") =》 --cc-dd + * + * @param str 字符串 + * @param strsToRemove 被移除的字符串 + * @return 移除后的字符串 + * @since 5.3.8 + */ + public static String removeAny(CharSequence str, CharSequence... strsToRemove) { + String result = str(str); + if (isNotEmpty(str)) { + for (CharSequence strToRemove : strsToRemove) { + result = removeAll(result, strToRemove); + } + } + return result; + } + + /** + * 去除字符串中指定的多个字符,如有多个则全部去除 + * + * @param str 字符串 + * @param chars 字符列表 + * @return 去除后的字符 + * @since 4.2.2 + */ + public static String removeAll(CharSequence str, char... chars) { + if (null == str || ArrayUtil.isEmpty(chars)) { + return str(str); + } + final int len = str.length(); + if (0 == len) { + return str(str); + } + final StringBuilder builder = new StringBuilder(len); + char c; + for (int i = 0; i < len; i++) { + c = str.charAt(i); + if (false == ArrayUtil.contains(chars, c)) { + builder.append(c); + } + } + return builder.toString(); + } + + /** + * 去除所有换行符,包括: + * + *
+	 * 1. \r
+	 * 1. \n
+	 * 
+ * + * @param str 字符串 + * @return 处理后的字符串 + * @since 4.2.2 + */ + public static String removeAllLineBreaks(CharSequence str) { + return removeAll(str, CharUtil.CR, CharUtil.LF); + } + + /** + * 去掉首部指定长度的字符串并将剩余字符串首字母小写
+ * 例如:str=setName, preLength=3 =》 return name + * + * @param str 被处理的字符串 + * @param preLength 去掉的长度 + * @return 处理后的字符串,不符合规范返回null + */ + public static String removePreAndLowerFirst(CharSequence str, int preLength) { + if (str == null) { + return null; + } + if (str.length() > preLength) { + char first = Character.toLowerCase(str.charAt(preLength)); + if (str.length() > preLength + 1) { + return first + str.toString().substring(preLength + 1); + } + return String.valueOf(first); + } else { + return str.toString(); + } + } + + /** + * 去掉首部指定长度的字符串并将剩余字符串首字母小写
+ * 例如:str=setName, prefix=set =》 return name + * + * @param str 被处理的字符串 + * @param prefix 前缀 + * @return 处理后的字符串,不符合规范返回null + */ + public static String removePreAndLowerFirst(CharSequence str, CharSequence prefix) { + return lowerFirst(removePrefix(str, prefix)); + } + + /** + * 去掉指定前缀 + * + * @param str 字符串 + * @param prefix 前缀 + * @return 切掉后的字符串,若前缀不是 preffix, 返回原字符串 + */ + public static String removePrefix(CharSequence str, CharSequence prefix) { + if (isEmpty(str) || isEmpty(prefix)) { + return str(str); + } + + final String str2 = str.toString(); + if (str2.startsWith(prefix.toString())) { + return subSuf(str2, prefix.length());// 截取后半段 + } + return str2; + } + + /** + * 忽略大小写去掉指定前缀 + * + * @param str 字符串 + * @param prefix 前缀 + * @return 切掉后的字符串,若前缀不是 prefix, 返回原字符串 + */ + public static String removePrefixIgnoreCase(CharSequence str, CharSequence prefix) { + if (isEmpty(str) || isEmpty(prefix)) { + return str(str); + } + + final String str2 = str.toString(); + if (str2.toLowerCase().startsWith(prefix.toString().toLowerCase())) { + return subSuf(str2, prefix.length());// 截取后半段 + } + return str2; + } + + /** + * 去掉指定后缀 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSuffix(CharSequence str, CharSequence suffix) { + if (isEmpty(str) || isEmpty(suffix)) { + return str(str); + } + + final String str2 = str.toString(); + if (str2.endsWith(suffix.toString())) { + return subPre(str2, str2.length() - suffix.length());// 截取前半段 + } + return str2; + } + + /** + * 去掉指定后缀,并小写首字母 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSufAndLowerFirst(CharSequence str, CharSequence suffix) { + return lowerFirst(removeSuffix(str, suffix)); + } + + /** + * 忽略大小写去掉指定后缀 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSuffixIgnoreCase(CharSequence str, CharSequence suffix) { + if (isEmpty(str) || isEmpty(suffix)) { + return str(str); + } + + final String str2 = str.toString(); + if (str2.toLowerCase().endsWith(suffix.toString().toLowerCase())) { + return subPre(str2, str2.length() - suffix.length()); + } + return str2; + } + + /** + * 清理空白字符 + * + * @param str 被清理的字符串 + * @return 清理后的字符串 + */ + public static String cleanBlank(CharSequence str) { + return filter(str, c -> false == CharUtil.isBlankChar(c)); + } + + // ------------------------------------------------------------------------ strip + + /** + * 去除两边的指定字符串 + * + * @param str 被处理的字符串 + * @param prefixOrSuffix 前缀或后缀 + * @return 处理后的字符串 + * @since 3.1.2 + */ + public static String strip(CharSequence str, CharSequence prefixOrSuffix) { + if (equals(str, prefixOrSuffix)) { + // 对于去除相同字符的情况单独处理 + return EMPTY; + } + return strip(str, prefixOrSuffix, prefixOrSuffix); + } + + /** + * 去除两边的指定字符串 + * + * @param str 被处理的字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 处理后的字符串 + * @since 3.1.2 + */ + public static String strip(CharSequence str, CharSequence prefix, CharSequence suffix) { + if (isEmpty(str)) { + return str(str); + } + + int from = 0; + int to = str.length(); + + String str2 = str.toString(); + if (startWith(str2, prefix)) { + from = prefix.length(); + } + if (endWith(str2, suffix)) { + to -= suffix.length(); + } + + return str2.substring(Math.min(from, to), Math.max(from, to)); + } + + /** + * 去除两边的指定字符串,忽略大小写 + * + * @param str 被处理的字符串 + * @param prefixOrSuffix 前缀或后缀 + * @return 处理后的字符串 + * @since 3.1.2 + */ + public static String stripIgnoreCase(CharSequence str, CharSequence prefixOrSuffix) { + return stripIgnoreCase(str, prefixOrSuffix, prefixOrSuffix); + } + + /** + * 去除两边的指定字符串,忽略大小写 + * + * @param str 被处理的字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 处理后的字符串 + * @since 3.1.2 + */ + public static String stripIgnoreCase(CharSequence str, CharSequence prefix, CharSequence suffix) { + if (isEmpty(str)) { + return str(str); + } + int from = 0; + int to = str.length(); + + String str2 = str.toString(); + if (startWithIgnoreCase(str2, prefix)) { + from = prefix.length(); + } + if (endWithIgnoreCase(str2, suffix)) { + to -= suffix.length(); + } + return str2.substring(from, to); + } + + // ------------------------------------------------------------------------ add + + /** + * 如果给定字符串不是以prefix开头的,在开头补充 prefix + * + * @param str 字符串 + * @param prefix 前缀 + * @return 补充后的字符串 + */ + public static String addPrefixIfNot(CharSequence str, CharSequence prefix) { + if (isEmpty(str) || isEmpty(prefix)) { + return str(str); + } + + final String str2 = str.toString(); + final String prefix2 = prefix.toString(); + if (false == str2.startsWith(prefix2)) { + return prefix2.concat(str2); + } + return str2; + } + + /** + * 如果给定字符串不是以suffix结尾的,在尾部补充 suffix + * + * @param str 字符串 + * @param suffix 后缀 + * @return 补充后的字符串 + */ + public static String addSuffixIfNot(CharSequence str, CharSequence suffix) { + if (isEmpty(str) || isEmpty(suffix)) { + return str(str); + } + + final String str2 = str.toString(); + final String suffix2 = suffix.toString(); + if (false == str2.endsWith(suffix2)) { + return str2.concat(suffix2); + } + return str2; + } + + // ------------------------------------------------------------------------ split + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的数组 + */ + public static String[] splitToArray(CharSequence str, char separator) { + return splitToArray(str, separator, 0); + } + + /** + * 切分字符串为long数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 切分后long数组 + * @since 4.0.6 + */ + public static long[] splitToLong(CharSequence str, char separator) { + return Convert.convert(long[].class, splitTrim(str, separator)); + } + + /** + * 切分字符串为long数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @return 切分后long数组 + * @since 4.0.6 + */ + public static long[] splitToLong(CharSequence str, CharSequence separator) { + return Convert.convert(long[].class, splitTrim(str, separator)); + } + + /** + * 切分字符串为int数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 切分后long数组 + * @since 4.0.6 + */ + public static int[] splitToInt(CharSequence str, char separator) { + return Convert.convert(int[].class, splitTrim(str, separator)); + } + + /** + * 切分字符串为int数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @return 切分后long数组 + * @since 4.0.6 + */ + public static int[] splitToInt(CharSequence str, CharSequence separator) { + return Convert.convert(int[].class, splitTrim(str, separator)); + } + + /** + * 切分字符串
+ * a#b#c =》 [a,b,c]
+ * a##b#c =》 [a,"",b,c] + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + */ + public static List split(CharSequence str, char separator) { + return split(str, separator, 0); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数 + * @return 切分后的数组 + */ + public static String[] splitToArray(CharSequence str, char separator, int limit) { + if (null == str) { + return new String[]{}; + } + return StrSpliter.splitToArray(str.toString(), separator, limit, false, false); + } + + /** + * 切分字符串,不去除切分后每个元素两边的空白符,不去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + */ + public static List split(CharSequence str, char separator, int limit) { + return split(str, separator, limit, false, false); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + * @since 3.1.2 + */ + public static List splitTrim(CharSequence str, char separator) { + return splitTrim(str, separator, -1); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List splitTrim(CharSequence str, CharSequence separator) { + return splitTrim(str, separator, -1); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + * @since 3.1.0 + */ + public static List splitTrim(CharSequence str, char separator, int limit) { + return split(str, separator, limit, true, true); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List splitTrim(CharSequence str, CharSequence separator, int limit) { + return split(str, separator, limit, true, true); + } + + /** + * 切分字符串,不限制分片数量 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, 0, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + if (null == str) { + return new ArrayList<>(0); + } + return StrSpliter.split(str.toString(), separator, limit, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) { + if (null == str) { + return new ArrayList<>(0); + } + final String separatorStr = (null == separator) ? null : separator.toString(); + return StrSpliter.split(str.toString(), separatorStr, limit, isTrim, ignoreEmpty); + } + + /** + * 切分字符串,如果分隔符不存在则返回原字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 字符串 + */ + public static String[] split(CharSequence str, CharSequence separator) { + if (str == null) { + return new String[]{}; + } + + final String separatorStr = (null == separator) ? null : separator.toString(); + return StrSpliter.splitToArray(str.toString(), separatorStr, 0, false, false); + } + + /** + * 根据给定长度,将给定字符串截取为多个部分 + * + * @param str 字符串 + * @param len 每一个小节的长度 + * @return 截取后的字符串数组 + * @see StrSpliter#splitByLength(String, int) + */ + public static String[] split(CharSequence str, int len) { + if (null == str) { + return new String[]{}; + } + return StrSpliter.splitByLength(str.toString(), len); + } + + /** + * 将字符串切分为N等份 + * + * @param str 字符串 + * @param partLength 每等份的长度 + * @return 切分后的数组 + * @since 3.0.6 + */ + public static String[] cut(CharSequence str, int partLength) { + if (null == str) { + return null; + } + int len = str.length(); + if (len < partLength) { + return new String[]{str.toString()}; + } + int part = NumberUtil.count(len, partLength); + final String[] array = new String[part]; + + final String str2 = str.toString(); + for (int i = 0; i < part; i++) { + array[i] = str2.substring(i * partLength, (i == part - 1) ? len : (partLength + i * partLength)); + } + return array; + } + + // ------------------------------------------------------------------------ sub + + /** + * 改进JDK subString
+ * index从0开始计算,最后一个字符为-1
+ * 如果from和to位置一样,返回 ""
+ * 如果from或to为负数,则按照length从后向前数位置,如果绝对值大于字符串长度,则from归到0,to归到length
+ * 如果经过修正的index中from大于to,则互换from和to example:
+ * abcdefgh 2 3 =》 c
+ * abcdefgh 2 -3 =》 cde
+ * + * @param str String + * @param fromIndexInclude 开始的index(包括) + * @param toIndexExclude 结束的index(不包括) + * @return 字串 + */ + public static String sub(CharSequence str, int fromIndexInclude, int toIndexExclude) { + if (isEmpty(str)) { + return str(str); + } + int len = str.length(); + + if (fromIndexInclude < 0) { + fromIndexInclude = len + fromIndexInclude; + if (fromIndexInclude < 0) { + fromIndexInclude = 0; + } + } else if (fromIndexInclude > len) { + fromIndexInclude = len; + } + + if (toIndexExclude < 0) { + toIndexExclude = len + toIndexExclude; + if (toIndexExclude < 0) { + toIndexExclude = len; + } + } else if (toIndexExclude > len) { + toIndexExclude = len; + } + + if (toIndexExclude < fromIndexInclude) { + int tmp = fromIndexInclude; + fromIndexInclude = toIndexExclude; + toIndexExclude = tmp; + } + + if (fromIndexInclude == toIndexExclude) { + return EMPTY; + } + + return str.toString().substring(fromIndexInclude, toIndexExclude); + } + + /** + * 通过CodePoint截取字符串,可以截断Emoji + * + * @param str String + * @param fromIndex 开始的index(包括) + * @param toIndex 结束的index(不包括) + * @return 字串 + */ + public static String subByCodePoint(CharSequence str, int fromIndex, int toIndex) { + if (isEmpty(str)) { + return str(str); + } + + if (fromIndex < 0 || fromIndex > toIndex) { + throw new IllegalArgumentException(); + } + + if (fromIndex == toIndex) { + return EMPTY; + } + + final StringBuilder sb = new StringBuilder(); + final int subLen = toIndex - fromIndex; + str.toString().codePoints().skip(fromIndex).limit(subLen).forEach(v -> sb.append(Character.toChars(v))); + return sb.toString(); + } + + /** + * 截取部分字符串,这里一个汉字的长度认为是2 + * + * @param str 字符串 + * @param len 切割的位置 + * @param suffix 切割后加上后缀 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subPreGbk(CharSequence str, int len, CharSequence suffix) { + if (isEmpty(str)) { + return str(str); + } + + byte[] b; + int counterOfDoubleByte = 0; + b = str.toString().getBytes(CharsetUtil.CHARSET_GBK); + if (b.length <= len) { + return str.toString(); + } + for (int i = 0; i < len; i++) { + if (b[i] < 0) { + counterOfDoubleByte++; + } + } + + if (counterOfDoubleByte % 2 != 0) { + len += 1; + } + return new String(b, 0, len, CharsetUtil.CHARSET_GBK) + suffix; + } + + /** + * 切割指定位置之前部分的字符串 + * + * @param string 字符串 + * @param toIndexExclude 切割到的位置(不包括) + * @return 切割后的剩余的前半部分字符串 + */ + public static String subPre(CharSequence string, int toIndexExclude) { + return sub(string, 0, toIndexExclude); + } + + /** + * 切割指定位置之后部分的字符串 + * + * @param string 字符串 + * @param fromIndex 切割开始的位置(包括) + * @return 切割后后剩余的后半部分字符串 + */ + public static String subSuf(CharSequence string, int fromIndex) { + if (isEmpty(string)) { + return null; + } + return sub(string, fromIndex, string.length()); + } + + /** + * 切割指定长度的后部分的字符串 + * + *
+	 * StrUtil.subSufByLength("abcde", 3)      =    "cde"
+	 * StrUtil.subSufByLength("abcde", 0)      =    ""
+	 * StrUtil.subSufByLength("abcde", -5)     =    ""
+	 * StrUtil.subSufByLength("abcde", -1)     =    ""
+	 * StrUtil.subSufByLength("abcde", 5)       =    "abcde"
+	 * StrUtil.subSufByLength("abcde", 10)     =    "abcde"
+	 * StrUtil.subSufByLength(null, 3)               =    null
+	 * 
+ * + * @param string 字符串 + * @param length 切割长度 + * @return 切割后后剩余的后半部分字符串 + * @since 4.0.1 + */ + public static String subSufByLength(CharSequence string, int length) { + if (isEmpty(string)) { + return null; + } + if (length <= 0) { + return EMPTY; + } + return sub(string, -length, string.length()); + } + + /** + * 截取字符串,从指定位置开始,截取指定长度的字符串
+ * author weibaohui + * + * @param input 原始字符串 + * @param fromIndex 开始的index,包括 + * @param length 要截取的长度 + * @return 截取后的字符串 + */ + public static String subWithLength(String input, int fromIndex, int length) { + return sub(input, fromIndex, fromIndex + length); + } + + /** + * 截取分隔字符串之前的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
+ * 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串,举例如下: + * + *
+	 * StrUtil.subBefore(null, *, false)      = null
+	 * StrUtil.subBefore("", *, false)        = ""
+	 * StrUtil.subBefore("abc", "a", false)   = ""
+	 * StrUtil.subBefore("abcba", "b", false) = "a"
+	 * StrUtil.subBefore("abc", "c", false)   = "ab"
+	 * StrUtil.subBefore("abc", "d", false)   = "abc"
+	 * StrUtil.subBefore("abc", "", false)    = ""
+	 * StrUtil.subBefore("abc", null, false)  = "abc"
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subBefore(CharSequence string, CharSequence separator, boolean isLastSeparator) { + if (isEmpty(string) || separator == null) { + return null == string ? null : string.toString(); + } + + final String str = string.toString(); + final String sep = separator.toString(); + if (sep.isEmpty()) { + return EMPTY; + } + final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); + if (INDEX_NOT_FOUND == pos) { + return str; + } + if (0 == pos) { + return EMPTY; + } + return str.substring(0, pos); + } + + /** + * 截取分隔字符串之前的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
+ * 如果分隔字符串未找到,返回原字符串,举例如下: + * + *
+	 * StrUtil.subBefore(null, *, false)      = null
+	 * StrUtil.subBefore("", *, false)        = ""
+	 * StrUtil.subBefore("abc", 'a', false)   = ""
+	 * StrUtil.subBefore("abcba", 'b', false) = "a"
+	 * StrUtil.subBefore("abc", 'c', false)   = "ab"
+	 * StrUtil.subBefore("abc", 'd', false)   = "abc"
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 4.1.15 + */ + public static String subBefore(CharSequence string, char separator, boolean isLastSeparator) { + if (isEmpty(string)) { + return null == string ? null : EMPTY; + } + + final String str = string.toString(); + final int pos = isLastSeparator ? str.lastIndexOf(separator) : str.indexOf(separator); + if (INDEX_NOT_FOUND == pos) { + return str; + } + if (0 == pos) { + return EMPTY; + } + return str.substring(0, pos); + } + + /** + * 截取分隔字符串之后的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或""),返回原字符串
+ * 如果分隔字符串为空串(null或""),则返回空串,如果分隔字符串未找到,返回空串,举例如下: + * + *
+	 * StrUtil.subAfter(null, *, false)      = null
+	 * StrUtil.subAfter("", *, false)        = ""
+	 * StrUtil.subAfter(*, null, false)      = ""
+	 * StrUtil.subAfter("abc", "a", false)   = "bc"
+	 * StrUtil.subAfter("abcba", "b", false) = "cba"
+	 * StrUtil.subAfter("abc", "c", false)   = ""
+	 * StrUtil.subAfter("abc", "d", false)   = ""
+	 * StrUtil.subAfter("abc", "", false)    = "abc"
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subAfter(CharSequence string, CharSequence separator, boolean isLastSeparator) { + if (isEmpty(string)) { + return null == string ? null : EMPTY; + } + if (separator == null) { + return EMPTY; + } + final String str = string.toString(); + final String sep = separator.toString(); + final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); + if (INDEX_NOT_FOUND == pos || (string.length() - 1) == pos) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } + + /** + * 截取分隔字符串之后的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或""),返回原字符串
+ * 如果分隔字符串为空串(null或""),则返回空串,如果分隔字符串未找到,返回空串,举例如下: + * + *
+	 * StrUtil.subAfter(null, *, false)      = null
+	 * StrUtil.subAfter("", *, false)        = ""
+	 * StrUtil.subAfter("abc", 'a', false)   = "bc"
+	 * StrUtil.subAfter("abcba", 'b', false) = "cba"
+	 * StrUtil.subAfter("abc", 'c', false)   = ""
+	 * StrUtil.subAfter("abc", 'd', false)   = ""
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 4.1.15 + */ + public static String subAfter(CharSequence string, char separator, boolean isLastSeparator) { + if (isEmpty(string)) { + return null == string ? null : EMPTY; + } + final String str = string.toString(); + final int pos = isLastSeparator ? str.lastIndexOf(separator) : str.indexOf(separator); + if (INDEX_NOT_FOUND == pos) { + return EMPTY; + } + return str.substring(pos + 1); + } + + /** + * 截取指定字符串中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StrUtil.subBetween("wx[b]yz", "[", "]") = "b"
+	 * StrUtil.subBetween(null, *, *)          = null
+	 * StrUtil.subBetween(*, null, *)          = null
+	 * StrUtil.subBetween(*, *, null)          = null
+	 * StrUtil.subBetween("", "", "")          = ""
+	 * StrUtil.subBetween("", "", "]")         = null
+	 * StrUtil.subBetween("", "[", "]")        = null
+	 * StrUtil.subBetween("yabcz", "", "")     = ""
+	 * StrUtil.subBetween("yabcz", "y", "z")   = "abc"
+	 * StrUtil.subBetween("yabczyabcz", "y", "z")   = "abc"
+	 * 
+ * + * @param str 被切割的字符串 + * @param before 截取开始的字符串标识 + * @param after 截取到的字符串标识 + * @return 截取后的字符串 + * @since 3.1.1 + */ + public static String subBetween(CharSequence str, CharSequence before, CharSequence after) { + if (str == null || before == null || after == null) { + return null; + } + + final String str2 = str.toString(); + final String before2 = before.toString(); + final String after2 = after.toString(); + + final int start = str2.indexOf(before2); + if (start != INDEX_NOT_FOUND) { + final int end = str2.indexOf(after2, start + before2.length()); + if (end != INDEX_NOT_FOUND) { + return str2.substring(start + before2.length(), end); + } + } + return null; + } + + /** + * 截取指定字符串中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StrUtil.subBetween(null, *)            = null
+	 * StrUtil.subBetween("", "")             = ""
+	 * StrUtil.subBetween("", "tag")          = null
+	 * StrUtil.subBetween("tagabctag", null)  = null
+	 * StrUtil.subBetween("tagabctag", "")    = ""
+	 * StrUtil.subBetween("tagabctag", "tag") = "abc"
+	 * 
+ * + * @param str 被切割的字符串 + * @param beforeAndAfter 截取开始和结束的字符串标识 + * @return 截取后的字符串 + * @since 3.1.1 + */ + public static String subBetween(CharSequence str, CharSequence beforeAndAfter) { + return subBetween(str, beforeAndAfter, beforeAndAfter); + } + + /** + * 截取指定字符串多段中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StrUtil.subBetweenAll("wx[b]y[z]", "[", "]") 		= ["b","z"]
+	 * StrUtil.subBetweenAll(null, *, *)          			= []
+	 * StrUtil.subBetweenAll(*, null, *)          			= []
+	 * StrUtil.subBetweenAll(*, *, null)          			= []
+	 * StrUtil.subBetweenAll("", "", "")          			= []
+	 * StrUtil.subBetweenAll("", "", "]")         			= []
+	 * StrUtil.subBetweenAll("", "[", "]")        			= []
+	 * StrUtil.subBetweenAll("yabcz", "", "")     			= []
+	 * StrUtil.subBetweenAll("yabcz", "y", "z")   			= ["abc"]
+	 * StrUtil.subBetweenAll("yabczyabcz", "y", "z")   		= ["abc","abc"]
+	 * StrUtil.subBetweenAll("[yabc[zy]abcz]", "[", "]");   = ["zy"]           重叠时只截取内部,
+	 * 
+ * + * @param str 被切割的字符串 + * @param prefix 截取开始的字符串标识 + * @param suffix 截取到的字符串标识 + * @return 截取后的字符串 + * @author dahuoyzs + * @since 5.2.5 + */ + public static String[] subBetweenAll(CharSequence str, CharSequence prefix, CharSequence suffix) { + if (hasEmpty(str, prefix, suffix) || + // 不包含起始字符串,则肯定没有子串 + false == contains(str, prefix)) { + return new String[0]; + } + + final List result = new LinkedList<>(); + final String[] split = split(str, prefix); + if (prefix.equals(suffix)) { + // 前后缀字符相同,单独处理 + for (int i = 1, length = split.length - 1; i < length; i += 2) { + result.add(split[i]); + } + } else { + int suffixIndex; + for (String fragment : split) { + suffixIndex = fragment.indexOf(suffix.toString()); + if (suffixIndex > 0) { + result.add(fragment.substring(0, suffixIndex)); + } + } + } + + return result.toArray(new String[0]); + } + + /** + * 截取指定字符串多段中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StrUtil.subBetweenAll(null, *)          			= []
+	 * StrUtil.subBetweenAll(*, null)          			= []
+	 * StrUtil.subBetweenAll(*, *)          			= []
+	 * StrUtil.subBetweenAll("", "")          			= []
+	 * StrUtil.subBetweenAll("", "#")         			= []
+	 * StrUtil.subBetweenAll("gotanks", "")     		= []
+	 * StrUtil.subBetweenAll("#gotanks#", "#")   		= ["gotanks"]
+	 * StrUtil.subBetweenAll("#hello# #world#!", "#")   = ["hello", "world"]
+	 * StrUtil.subBetweenAll("#hello# world#!", "#");   = ["hello"]
+	 * 
+ * + * @param str 被切割的字符串 + * @param prefixAndSuffix 截取开始和结束的字符串标识 + * @return 截取后的字符串 + * @author gotanks + * @since 5.5.0 + */ + public static String[] subBetweenAll(CharSequence str, CharSequence prefixAndSuffix) { + return subBetweenAll(str, prefixAndSuffix, prefixAndSuffix); + } + + // ------------------------------------------------------------------------ repeat + + /** + * 重复某个字符 + * + * @param c 被重复的字符 + * @param count 重复的数目,如果小于等于0则返回"" + * @return 重复字符字符串 + */ + public static String repeat(char c, int count) { + if (count <= 0) { + return EMPTY; + } + + char[] result = new char[count]; + for (int i = 0; i < count; i++) { + result[i] = c; + } + return new String(result); + } + + /** + * 重复某个字符串 + * + * @param str 被重复的字符 + * @param count 重复的数目 + * @return 重复字符字符串 + */ + public static String repeat(CharSequence str, int count) { + if (null == str) { + return null; + } + if (count <= 0 || str.length() == 0) { + return EMPTY; + } + if (count == 1) { + return str.toString(); + } + + // 检查 + final int len = str.length(); + final long longSize = (long) len * (long) count; + final int size = (int) longSize; + if (size != longSize) { + throw new ArrayIndexOutOfBoundsException("Required String length is too large: " + longSize); + } + + final char[] array = new char[size]; + str.toString().getChars(0, len, array, 0); + int n; + for (n = len; n < size - n; n <<= 1) {// n <<= 1相当于n *2 + System.arraycopy(array, 0, array, n, n); + } + System.arraycopy(array, 0, array, n, size - n); + return new String(array); + } + + /** + * 重复某个字符串到指定长度 + * + * @param str 被重复的字符 + * @param padLen 指定长度 + * @return 重复字符字符串 + * @since 4.3.2 + */ + public static String repeatByLength(CharSequence str, int padLen) { + if (null == str) { + return null; + } + if (padLen <= 0) { + return StrUtil.EMPTY; + } + final int strLen = str.length(); + if (strLen == padLen) { + return str.toString(); + } else if (strLen > padLen) { + return subPre(str, padLen); + } + + // 重复,直到达到指定长度 + final char[] padding = new char[padLen]; + for (int i = 0; i < padLen; i++) { + padding[i] = str.charAt(i % strLen); + } + return new String(padding); + } + + /** + * 重复某个字符串并通过分界符连接 + * + *
+	 * StrUtil.repeatAndJoin("?", 5, ",")   = "?,?,?,?,?"
+	 * StrUtil.repeatAndJoin("?", 0, ",")   = ""
+	 * StrUtil.repeatAndJoin("?", 5, null) = "?????"
+	 * 
+ * + * @param str 被重复的字符串 + * @param count 数量 + * @param conjunction 分界符 + * @return 连接后的字符串 + * @since 4.0.1 + */ + public static String repeatAndJoin(CharSequence str, int count, CharSequence conjunction) { + if (count <= 0) { + return EMPTY; + } + final StrBuilder builder = StrBuilder.create(); + boolean isFirst = true; + while (count-- > 0) { + if (isFirst) { + isFirst = false; + } else if (isNotEmpty(conjunction)) { + builder.append(conjunction); + } + builder.append(str); + } + return builder.toString(); + } + + // ------------------------------------------------------------------------ equals + + /** + * 比较两个字符串(大小写敏感)。 + * + *
+	 * equals(null, null)   = true
+	 * equals(null, "abc")  = false
+	 * equals("abc", null)  = false
+	 * equals("abc", "abc") = true
+	 * equals("abc", "ABC") = false
+	 * 
+ * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} + */ + public static boolean equals(CharSequence str1, CharSequence str2) { + return equals(str1, str2, false); + } + + /** + * 比较两个字符串(大小写不敏感)。 + * + *
+	 * equalsIgnoreCase(null, null)   = true
+	 * equalsIgnoreCase(null, "abc")  = false
+	 * equalsIgnoreCase("abc", null)  = false
+	 * equalsIgnoreCase("abc", "abc") = true
+	 * equalsIgnoreCase("abc", "ABC") = true
+	 * 
+ * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} + */ + public static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) { + return equals(str1, str2, true); + } + + /** + * 比较两个字符串是否相等。 + * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @param ignoreCase 是否忽略大小写 + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} + * @since 3.2.0 + */ + public static boolean equals(CharSequence str1, CharSequence str2, boolean ignoreCase) { + if (null == str1) { + // 只有两个都为null才判断相等 + return str2 == null; + } + if (null == str2) { + // 字符串2空,字符串1非空,直接false + return false; + } + + if (ignoreCase) { + return str1.toString().equalsIgnoreCase(str2.toString()); + } else { + return str1.toString().contentEquals(str2); + } + } + + /** + * 给定字符串是否与提供的中任一字符串相同(忽略大小写),相同则返回{@code true},没有相同的返回{@code false}
+ * 如果参与比对的字符串列表为空,返回{@code false} + * + * @param str1 给定需要检查的字符串 + * @param strs 需要参与比对的字符串列表 + * @return 是否相同 + * @since 4.3.2 + */ + public static boolean equalsAnyIgnoreCase(CharSequence str1, CharSequence... strs) { + return equalsAny(str1, true, strs); + } + + /** + * 给定字符串是否与提供的中任一字符串相同,相同则返回{@code true},没有相同的返回{@code false}
+ * 如果参与比对的字符串列表为空,返回{@code false} + * + * @param str1 给定需要检查的字符串 + * @param strs 需要参与比对的字符串列表 + * @return 是否相同 + * @since 4.3.2 + */ + public static boolean equalsAny(CharSequence str1, CharSequence... strs) { + return equalsAny(str1, false, strs); + } + + /** + * 给定字符串是否与提供的中任一字符串相同,相同则返回{@code true},没有相同的返回{@code false}
+ * 如果参与比对的字符串列表为空,返回{@code false} + * + * @param str1 给定需要检查的字符串 + * @param ignoreCase 是否忽略大小写 + * @param strs 需要参与比对的字符串列表 + * @return 是否相同 + * @since 4.3.2 + */ + public static boolean equalsAny(CharSequence str1, boolean ignoreCase, CharSequence... strs) { + if (ArrayUtil.isEmpty(strs)) { + return false; + } + + for (CharSequence str : strs) { + if (equals(str1, str, ignoreCase)) { + return true; + } + } + return false; + } + + /** + * 字符串指定位置的字符是否与给定字符相同
+ * 如果字符串为null,返回false
+ * 如果给定的位置大于字符串长度,返回false
+ * 如果给定的位置小于0,返回false + * + * @param str 字符串 + * @param position 位置 + * @param c 需要对比的字符 + * @return 字符串指定位置的字符是否与给定字符相同 + * @since 3.3.1 + */ + public static boolean equalsCharAt(CharSequence str, int position, char c) { + if (null == str || position < 0) { + return false; + } + return str.length() > position && c == str.charAt(position); + } + + /** + * 截取两个字符串的不同部分(长度一致),判断截取的子串是否相同
+ * 任意一个字符串为null返回false + * + * @param str1 第一个字符串 + * @param start1 第一个字符串开始的位置 + * @param str2 第二个字符串 + * @param start2 第二个字符串开始的位置 + * @param length 截取长度 + * @param ignoreCase 是否忽略大小写 + * @return 子串是否相同 + * @since 3.2.1 + */ + public static boolean isSubEquals(CharSequence str1, int start1, CharSequence str2, int start2, int length, boolean ignoreCase) { + if (null == str1 || null == str2) { + return false; + } + + return str1.toString().regionMatches(ignoreCase, start1, str2.toString(), start2, length); + } + + // ------------------------------------------------------------------------ format + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") =》 this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") =》 this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") =》 this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示,如果模板为null,返回"null" + * @param params 参数值 + * @return 格式化后的文本,如果模板为null,返回"null" + */ + public static String format(CharSequence template, Object... params) { + if (null == template) { + return NULL; + } + if (ArrayUtil.isEmpty(params) || isBlank(template)) { + return template.toString(); + } + return StrFormatter.format(template.toString(), params); + } + + /** + * 有序的格式化文本,使用{number}做为占位符
+ * 通常使用:format("this is {0} for {1}", "a", "b") =》 this is a for b
+ * + * @param pattern 文本格式 + * @param arguments 参数 + * @return 格式化后的文本 + */ + public static String indexedFormat(CharSequence pattern, Object... arguments) { + return MessageFormat.format(pattern.toString(), arguments); + } + // ------------------------------------------------------------------------ bytes + + /** + * 编码字符串,编码为UTF-8 + * + * @param str 字符串 + * @return 编码后的字节码 + */ + public static byte[] utf8Bytes(CharSequence str) { + return bytes(str, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 编码字符串
+ * 使用系统默认编码 + * + * @param str 字符串 + * @return 编码后的字节码 + */ + public static byte[] bytes(CharSequence str) { + return bytes(str, Charset.defaultCharset()); + } + + /** + * 编码字符串 + * + * @param str 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 编码后的字节码 + */ + public static byte[] bytes(CharSequence str, String charset) { + return bytes(str, isBlank(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 编码字符串 + * + * @param str 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 编码后的字节码 + */ + public static byte[] bytes(CharSequence str, Charset charset) { + if (str == null) { + return null; + } + + if (null == charset) { + return str.toString().getBytes(); + } + return str.toString().getBytes(charset); + } + + /** + * 字符串转换为byteBuffer + * + * @param str 字符串 + * @param charset 编码 + * @return byteBuffer + */ + public static ByteBuffer byteBuffer(CharSequence str, String charset) { + return ByteBuffer.wrap(bytes(str, charset)); + } + + // ------------------------------------------------------------------------ wrap + + /** + * 包装指定字符串
+ * 当前缀和后缀一致时使用此方法 + * + * @param str 被包装的字符串 + * @param prefixAndSuffix 前缀和后缀 + * @return 包装后的字符串 + * @since 3.1.0 + */ + public static String wrap(CharSequence str, CharSequence prefixAndSuffix) { + return wrap(str, prefixAndSuffix, prefixAndSuffix); + } + + /** + * 包装指定字符串 + * + * @param str 被包装的字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 包装后的字符串 + */ + public static String wrap(CharSequence str, CharSequence prefix, CharSequence suffix) { + return nullToEmpty(prefix).concat(nullToEmpty(str)).concat(nullToEmpty(suffix)); + } + + /** + * 使用单个字符包装多个字符串 + * + * @param prefixAndSuffix 前缀和后缀 + * @param strs 多个字符串 + * @return 包装的字符串数组 + * @since 5.4.1 + */ + public static String[] wrapAllWithPair(CharSequence prefixAndSuffix, CharSequence... strs) { + return wrapAll(prefixAndSuffix, prefixAndSuffix, strs); + } + + /** + * 包装多个字符串 + * + * @param prefix 前缀 + * @param suffix 后缀 + * @param strs 多个字符串 + * @return 包装的字符串数组 + * @since 4.0.7 + */ + public static String[] wrapAll(CharSequence prefix, CharSequence suffix, CharSequence... strs) { + final String[] results = new String[strs.length]; + for (int i = 0; i < strs.length; i++) { + results[i] = wrap(strs[i], prefix, suffix); + } + return results; + } + + /** + * 包装指定字符串,如果前缀或后缀已经包含对应的字符串,则不再包装 + * + * @param str 被包装的字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 包装后的字符串 + */ + public static String wrapIfMissing(CharSequence str, CharSequence prefix, CharSequence suffix) { + int len = 0; + if (isNotEmpty(str)) { + len += str.length(); + } + if (isNotEmpty(prefix)) { + len += str.length(); + } + if (isNotEmpty(suffix)) { + len += str.length(); + } + StringBuilder sb = new StringBuilder(len); + if (isNotEmpty(prefix) && false == startWith(str, prefix)) { + sb.append(prefix); + } + if (isNotEmpty(str)) { + sb.append(str); + } + if (isNotEmpty(suffix) && false == endWith(str, suffix)) { + sb.append(suffix); + } + return sb.toString(); + } + + /** + * 使用成对的字符包装多个字符串,如果已经包装,则不再包装 + * + * @param prefixAndSuffix 前缀和后缀 + * @param strs 多个字符串 + * @return 包装的字符串数组 + * @since 5.4.1 + */ + public static String[] wrapAllWithPairIfMissing(CharSequence prefixAndSuffix, CharSequence... strs) { + return wrapAllIfMissing(prefixAndSuffix, prefixAndSuffix, strs); + } + + /** + * 包装多个字符串,如果已经包装,则不再包装 + * + * @param prefix 前缀 + * @param suffix 后缀 + * @param strs 多个字符串 + * @return 包装的字符串数组 + * @since 4.0.7 + */ + public static String[] wrapAllIfMissing(CharSequence prefix, CharSequence suffix, CharSequence... strs) { + final String[] results = new String[strs.length]; + for (int i = 0; i < strs.length; i++) { + results[i] = wrapIfMissing(strs[i], prefix, suffix); + } + return results; + } + + /** + * 去掉字符包装,如果未被包装则返回原字符串 + * + * @param str 字符串 + * @param prefix 前置字符串 + * @param suffix 后置字符串 + * @return 去掉包装字符的字符串 + * @since 4.0.1 + */ + public static String unWrap(CharSequence str, String prefix, String suffix) { + if (isWrap(str, prefix, suffix)) { + return sub(str, prefix.length(), str.length() - suffix.length()); + } + return str.toString(); + } + + /** + * 去掉字符包装,如果未被包装则返回原字符串 + * + * @param str 字符串 + * @param prefix 前置字符 + * @param suffix 后置字符 + * @return 去掉包装字符的字符串 + * @since 4.0.1 + */ + public static String unWrap(CharSequence str, char prefix, char suffix) { + if (isEmpty(str)) { + return str(str); + } + if (str.charAt(0) == prefix && str.charAt(str.length() - 1) == suffix) { + return sub(str, 1, str.length() - 1); + } + return str.toString(); + } + + /** + * 去掉字符包装,如果未被包装则返回原字符串 + * + * @param str 字符串 + * @param prefixAndSuffix 前置和后置字符 + * @return 去掉包装字符的字符串 + * @since 4.0.1 + */ + public static String unWrap(CharSequence str, char prefixAndSuffix) { + return unWrap(str, prefixAndSuffix, prefixAndSuffix); + } + + /** + * 指定字符串是否被包装 + * + * @param str 字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 是否被包装 + */ + public static boolean isWrap(CharSequence str, String prefix, String suffix) { + if (ArrayUtil.hasNull(str, prefix, suffix)) { + return false; + } + final String str2 = str.toString(); + return str2.startsWith(prefix) && str2.endsWith(suffix); + } + + /** + * 指定字符串是否被同一字符包装(前后都有这些字符串) + * + * @param str 字符串 + * @param wrapper 包装字符串 + * @return 是否被包装 + */ + public static boolean isWrap(CharSequence str, String wrapper) { + return isWrap(str, wrapper, wrapper); + } + + /** + * 指定字符串是否被同一字符包装(前后都有这些字符串) + * + * @param str 字符串 + * @param wrapper 包装字符 + * @return 是否被包装 + */ + public static boolean isWrap(CharSequence str, char wrapper) { + return isWrap(str, wrapper, wrapper); + } + + /** + * 指定字符串是否被包装 + * + * @param str 字符串 + * @param prefixChar 前缀 + * @param suffixChar 后缀 + * @return 是否被包装 + */ + public static boolean isWrap(CharSequence str, char prefixChar, char suffixChar) { + if (null == str) { + return false; + } + + return str.charAt(0) == prefixChar && str.charAt(str.length() - 1) == suffixChar; + } + + // ------------------------------------------------------------------------ pad + + /** + * 补充字符串以满足最小长度 + * + *
+	 * StrUtil.padPre(null, *, *);//null
+	 * StrUtil.padPre("1", 3, "ABC");//"AB1"
+	 * StrUtil.padPre("123", 2, "ABC");//"12"
+	 * 
+ * + * @param str 字符串 + * @param minLength 最小长度 + * @param padStr 补充的字符 + * @return 补充后的字符串 + */ + public static String padPre(CharSequence str, int minLength, CharSequence padStr) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == minLength) { + return str.toString(); + } else if (strLen > minLength) { + return subPre(str, minLength); + } + + return repeatByLength(padStr, minLength - strLen).concat(str.toString()); + } + + /** + * 补充字符串以满足最小长度 + * + *
+	 * StrUtil.padPre(null, *, *);//null
+	 * StrUtil.padPre("1", 3, '0');//"001"
+	 * StrUtil.padPre("123", 2, '0');//"12"
+	 * 
+ * + * @param str 字符串 + * @param minLength 最小长度 + * @param padChar 补充的字符 + * @return 补充后的字符串 + */ + public static String padPre(CharSequence str, int minLength, char padChar) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == minLength) { + return str.toString(); + } else if (strLen > minLength) { + return subPre(str, minLength); + } + + return repeat(padChar, minLength - strLen).concat(str.toString()); + } + + /** + * 补充字符串以满足最小长度 + * + *
+	 * StrUtil.padAfter(null, *, *);//null
+	 * StrUtil.padAfter("1", 3, '0');//"100"
+	 * StrUtil.padAfter("123", 2, '0');//"23"
+	 * 
+ * + * @param str 字符串,如果为{@code null},直接返回null + * @param minLength 最小长度 + * @param padChar 补充的字符 + * @return 补充后的字符串 + */ + public static String padAfter(CharSequence str, int minLength, char padChar) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == minLength) { + return str.toString(); + } else if (strLen > minLength) { + return sub(str, strLen - minLength, strLen); + } + + return str.toString().concat(repeat(padChar, minLength - strLen)); + } + + /** + * 补充字符串以满足最小长度 + * + *
+	 * StrUtil.padAfter(null, *, *);//null
+	 * StrUtil.padAfter("1", 3, "ABC");//"1AB"
+	 * StrUtil.padAfter("123", 2, "ABC");//"23"
+	 * 
+ * + * @param str 字符串,如果为{@code null},直接返回null + * @param minLength 最小长度 + * @param padStr 补充的字符 + * @return 补充后的字符串 + * @since 4.3.2 + */ + public static String padAfter(CharSequence str, int minLength, CharSequence padStr) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == minLength) { + return str.toString(); + } else if (strLen > minLength) { + return subSufByLength(str, minLength); + } + + return str.toString().concat(repeatByLength(padStr, minLength - strLen)); + } + + // ------------------------------------------------------------------------ center + + /** + * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 + * + *
+	 * StrUtil.center(null, *)   = null
+	 * StrUtil.center("", 4)     = "    "
+	 * StrUtil.center("ab", -1)  = "ab"
+	 * StrUtil.center("ab", 4)   = " ab "
+	 * StrUtil.center("abcd", 2) = "abcd"
+	 * StrUtil.center("a", 4)    = " a  "
+	 * 
+ * + * @param str 字符串 + * @param size 指定长度 + * @return 补充后的字符串 + * @since 4.3.2 + */ + public static String center(CharSequence str, final int size) { + return center(str, size, CharUtil.SPACE); + } + + /** + * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 + * + *
+	 * StrUtil.center(null, *, *)     = null
+	 * StrUtil.center("", 4, ' ')     = "    "
+	 * StrUtil.center("ab", -1, ' ')  = "ab"
+	 * StrUtil.center("ab", 4, ' ')   = " ab "
+	 * StrUtil.center("abcd", 2, ' ') = "abcd"
+	 * StrUtil.center("a", 4, ' ')    = " a  "
+	 * StrUtil.center("a", 4, 'y')   = "yayy"
+	 * StrUtil.center("abc", 7, ' ')   = "  abc  "
+	 * 
+ * + * @param str 字符串 + * @param size 指定长度 + * @param padChar 两边补充的字符 + * @return 补充后的字符串 + * @since 4.3.2 + */ + public static String center(CharSequence str, final int size, char padChar) { + if (str == null || size <= 0) { + return str(str); + } + final int strLen = str.length(); + final int pads = size - strLen; + if (pads <= 0) { + return str.toString(); + } + str = padPre(str, strLen + pads / 2, padChar); + str = padAfter(str, size, padChar); + return str.toString(); + } + + /** + * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 + * + *
+	 * StrUtil.center(null, *, *)     = null
+	 * StrUtil.center("", 4, " ")     = "    "
+	 * StrUtil.center("ab", -1, " ")  = "ab"
+	 * StrUtil.center("ab", 4, " ")   = " ab "
+	 * StrUtil.center("abcd", 2, " ") = "abcd"
+	 * StrUtil.center("a", 4, " ")    = " a  "
+	 * StrUtil.center("a", 4, "yz")   = "yayz"
+	 * StrUtil.center("abc", 7, null) = "  abc  "
+	 * StrUtil.center("abc", 7, "")   = "  abc  "
+	 * 
+ * + * @param str 字符串 + * @param size 指定长度 + * @param padStr 两边补充的字符串 + * @return 补充后的字符串 + */ + public static String center(CharSequence str, final int size, CharSequence padStr) { + if (str == null || size <= 0) { + return str(str); + } + if (isEmpty(padStr)) { + padStr = SPACE; + } + final int strLen = str.length(); + final int pads = size - strLen; + if (pads <= 0) { + return str.toString(); + } + str = padPre(str, strLen + pads / 2, padStr); + str = padAfter(str, size, padStr); + return str.toString(); + } + + // ------------------------------------------------------------------------ str + + /** + * {@link CharSequence} 转为字符串,null安全 + * + * @param cs {@link CharSequence} + * @return 字符串 + */ + public static String str(CharSequence cs) { + return null == cs ? null : cs.toString(); + } + + // ------------------------------------------------------------------------ count + + /** + * 统计指定内容中包含指定字符串的数量
+ * 参数为 {@code null} 或者 "" 返回 {@code 0}. + * + *
+	 * StrUtil.count(null, *)       = 0
+	 * StrUtil.count("", *)         = 0
+	 * StrUtil.count("abba", null)  = 0
+	 * StrUtil.count("abba", "")    = 0
+	 * StrUtil.count("abba", "a")   = 2
+	 * StrUtil.count("abba", "ab")  = 1
+	 * StrUtil.count("abba", "xxx") = 0
+	 * 
+ * + * @param content 被查找的字符串 + * @param strForSearch 需要查找的字符串 + * @return 查找到的个数 + */ + public static int count(CharSequence content, CharSequence strForSearch) { + if (hasEmpty(content, strForSearch) || strForSearch.length() > content.length()) { + return 0; + } + + int count = 0; + int idx = 0; + final String content2 = content.toString(); + final String strForSearch2 = strForSearch.toString(); + while ((idx = content2.indexOf(strForSearch2, idx)) > -1) { + count++; + idx += strForSearch.length(); + } + return count; + } + + /** + * 统计指定内容中包含指定字符的数量 + * + * @param content 内容 + * @param charForSearch 被统计的字符 + * @return 包含数量 + */ + public static int count(CharSequence content, char charForSearch) { + int count = 0; + if (isEmpty(content)) { + return 0; + } + int contentLength = content.length(); + for (int i = 0; i < contentLength; i++) { + if (charForSearch == content.charAt(i)) { + count++; + } + } + return count; + } + + // ------------------------------------------------------------------------ compare + + /** + * 比较两个字符串,用于排序 + * + *
+	 * StrUtil.compare(null, null, *)     = 0
+	 * StrUtil.compare(null , "a", true)  < 0
+	 * StrUtil.compare(null , "a", false) > 0
+	 * StrUtil.compare("a", null, true)   > 0
+	 * StrUtil.compare("a", null, false)  < 0
+	 * StrUtil.compare("abc", "abc", *)   = 0
+	 * StrUtil.compare("a", "b", *)       < 0
+	 * StrUtil.compare("b", "a", *)       > 0
+	 * StrUtil.compare("a", "B", *)       > 0
+	 * StrUtil.compare("ab", "abc", *)    < 0
+	 * 
+ * + * @param str1 字符串1 + * @param str2 字符串2 + * @param nullIsLess {@code null} 值是否排在前(null是否小于非空值) + * @return 排序值。负数:str1 < str2,正数:str1 > str2, 0:str1 == str2 + */ + public static int compare(final CharSequence str1, final CharSequence str2, final boolean nullIsLess) { + if (str1 == str2) { + return 0; + } + if (str1 == null) { + return nullIsLess ? -1 : 1; + } + if (str2 == null) { + return nullIsLess ? 1 : -1; + } + return str1.toString().compareTo(str2.toString()); + } + + /** + * 比较两个字符串,用于排序,大小写不敏感 + * + *
+	 * StrUtil.compareIgnoreCase(null, null, *)     = 0
+	 * StrUtil.compareIgnoreCase(null , "a", true)  < 0
+	 * StrUtil.compareIgnoreCase(null , "a", false) > 0
+	 * StrUtil.compareIgnoreCase("a", null, true)   > 0
+	 * StrUtil.compareIgnoreCase("a", null, false)  < 0
+	 * StrUtil.compareIgnoreCase("abc", "abc", *)   = 0
+	 * StrUtil.compareIgnoreCase("abc", "ABC", *)   = 0
+	 * StrUtil.compareIgnoreCase("a", "b", *)       < 0
+	 * StrUtil.compareIgnoreCase("b", "a", *)       > 0
+	 * StrUtil.compareIgnoreCase("a", "B", *)       < 0
+	 * StrUtil.compareIgnoreCase("A", "b", *)       < 0
+	 * StrUtil.compareIgnoreCase("ab", "abc", *)    < 0
+	 * 
+ * + * @param str1 字符串1 + * @param str2 字符串2 + * @param nullIsLess {@code null} 值是否排在前(null是否小于非空值) + * @return 排序值。负数:str1 < str2,正数:str1 > str2, 0:str1 == str2 + */ + public static int compareIgnoreCase(CharSequence str1, CharSequence str2, boolean nullIsLess) { + if (str1 == str2) { + return 0; + } + if (str1 == null) { + return nullIsLess ? -1 : 1; + } + if (str2 == null) { + return nullIsLess ? 1 : -1; + } + return str1.toString().compareToIgnoreCase(str2.toString()); + } + + /** + * 比较两个版本
+ * null版本排在最小:即: + * + *
+	 * StrUtil.compareVersion(null, "v1") < 0
+	 * StrUtil.compareVersion("v1", "v1")  = 0
+	 * StrUtil.compareVersion(null, null)   = 0
+	 * StrUtil.compareVersion("v1", null) > 0
+	 * StrUtil.compareVersion("1.0.0", "1.0.2") < 0
+	 * StrUtil.compareVersion("1.0.2", "1.0.2a") < 0
+	 * StrUtil.compareVersion("1.13.0", "1.12.1c") > 0
+	 * StrUtil.compareVersion("V0.0.20170102", "V0.0.20170101") > 0
+	 * 
+ * + * @param version1 版本1 + * @param version2 版本2 + * @return 排序值。负数:version1 < version2,正数:version1 > version2, 0:version1 == version2 + * @since 4.0.2 + */ + public static int compareVersion(CharSequence version1, CharSequence version2) { + return VersionComparator.INSTANCE.compare(str(version1), str(version2)); + } + + // ------------------------------------------------------------------------ append and prepend + + /** + * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串
+ * 不忽略大小写 + * + * @param str 被检查的字符串 + * @param suffix 需要添加到结尾的字符串 + * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String appendIfMissing(final CharSequence str, final CharSequence suffix, final CharSequence... suffixes) { + return appendIfMissing(str, suffix, false, suffixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串
+ * 忽略大小写 + * + * @param str 被检查的字符串 + * @param suffix 需要添加到结尾的字符串 + * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String appendIfMissingIgnoreCase(final CharSequence str, final CharSequence suffix, final CharSequence... suffixes) { + return appendIfMissing(str, suffix, true, suffixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串 + * + * @param str 被检查的字符串 + * @param suffix 需要添加到结尾的字符串 + * @param ignoreCase 检查结尾时是否忽略大小写 + * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String appendIfMissing(final CharSequence str, final CharSequence suffix, final boolean ignoreCase, final CharSequence... suffixes) { + if (str == null || isEmpty(suffix) || endWith(str, suffix, ignoreCase)) { + return str(str); + } + if (suffixes != null && suffixes.length > 0) { + for (final CharSequence s : suffixes) { + if (endWith(str, s, ignoreCase)) { + return str.toString(); + } + } + } + return str.toString().concat(suffix.toString()); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串
+ * 不忽略大小写 + * + * @param str 被检查的字符串 + * @param prefix 需要添加到首部的字符串 + * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String prependIfMissing(final CharSequence str, final CharSequence prefix, final CharSequence... prefixes) { + return prependIfMissing(str, prefix, false, prefixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串
+ * 忽略大小写 + * + * @param str 被检查的字符串 + * @param prefix 需要添加到首部的字符串 + * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String prependIfMissingIgnoreCase(final CharSequence str, final CharSequence prefix, final CharSequence... prefixes) { + return prependIfMissing(str, prefix, true, prefixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串 + * + * @param str 被检查的字符串 + * @param prefix 需要添加到首部的字符串 + * @param ignoreCase 检查结尾时是否忽略大小写 + * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String prependIfMissing(final CharSequence str, final CharSequence prefix, final boolean ignoreCase, final CharSequence... prefixes) { + if (str == null || isEmpty(prefix) || startWith(str, prefix, ignoreCase)) { + return str(str); + } + if (prefixes != null && prefixes.length > 0) { + for (final CharSequence s : prefixes) { + if (startWith(str, s, ignoreCase)) { + return str.toString(); + } + } + } + return prefix.toString().concat(str.toString()); + } + + // ------------------------------------------------------------------------ replace + + /** + * 替换字符串中的指定字符串,忽略大小写 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacement 被替换的字符串 + * @return 替换后的字符串 + * @since 4.0.3 + */ + public static String replaceIgnoreCase(CharSequence str, CharSequence searchStr, CharSequence replacement) { + return replace(str, 0, searchStr, replacement, true); + } + + /** + * 替换字符串中的指定字符串 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacement 被替换的字符串 + * @return 替换后的字符串 + * @since 4.0.3 + */ + public static String replace(CharSequence str, CharSequence searchStr, CharSequence replacement) { + return replace(str, 0, searchStr, replacement, false); + } + + /** + * 替换字符串中的指定字符串 + * + * @param str 字符串 + * @param searchStr 被查找的字符串 + * @param replacement 被替换的字符串 + * @param ignoreCase 是否忽略大小写 + * @return 替换后的字符串 + * @since 4.0.3 + */ + public static String replace(CharSequence str, CharSequence searchStr, CharSequence replacement, boolean ignoreCase) { + return replace(str, 0, searchStr, replacement, ignoreCase); + } + + /** + * 替换字符串中的指定字符串 + * + * @param str 字符串 + * @param fromIndex 开始位置(包括) + * @param searchStr 被查找的字符串 + * @param replacement 被替换的字符串 + * @param ignoreCase 是否忽略大小写 + * @return 替换后的字符串 + * @since 4.0.3 + */ + public static String replace(CharSequence str, int fromIndex, CharSequence searchStr, CharSequence replacement, boolean ignoreCase) { + if (isEmpty(str) || isEmpty(searchStr)) { + return str(str); + } + if (null == replacement) { + replacement = EMPTY; + } + + final int strLength = str.length(); + final int searchStrLength = searchStr.length(); + if (fromIndex > strLength) { + return str(str); + } else if (fromIndex < 0) { + fromIndex = 0; + } + + final StrBuilder result = StrBuilder.create(strLength + 16); + if (0 != fromIndex) { + result.append(str.subSequence(0, fromIndex)); + } + + int preIndex = fromIndex; + int index; + while ((index = indexOf(str, searchStr, preIndex, ignoreCase)) > -1) { + result.append(str.subSequence(preIndex, index)); + result.append(replacement); + preIndex = index + searchStrLength; + } + + if (preIndex < strLength) { + // 结尾部分 + result.append(str.subSequence(preIndex, strLength)); + } + return result.toString(); + } + + /** + * 替换指定字符串的指定区间内字符为固定字符 + * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @param replacedChar 被替换的字符 + * @return 替换后的字符串 + * @since 3.2.1 + */ + public static String replace(CharSequence str, int startInclude, int endExclude, char replacedChar) { + if (isEmpty(str)) { + return str(str); + } + final int strLength = str.length(); + if (startInclude > strLength) { + return str(str); + } + if (endExclude > strLength) { + endExclude = strLength; + } + if (startInclude > endExclude) { + // 如果起始位置大于结束位置,不替换 + return str(str); + } + + final char[] chars = new char[strLength]; + for (int i = 0; i < strLength; i++) { + if (i >= startInclude && i < endExclude) { + chars[i] = replacedChar; + } else { + chars[i] = str.charAt(i); + } + } + return new String(chars); + } + + /** + * 替换所有正则匹配的文本,并使用自定义函数决定如何替换 + * + * @param str 要替换的字符串 + * @param pattern 用于匹配的正则式 + * @param replaceFun 决定如何替换的函数 + * @return 替换后的字符串 + * @see ReUtil#replaceAll(CharSequence, java.util.regex.Pattern, Func1) + * @since 4.2.2 + */ + public static String replace(CharSequence str, java.util.regex.Pattern pattern, Func1 replaceFun) { + return ReUtil.replaceAll(str, pattern, replaceFun); + } + + /** + * 替换所有正则匹配的文本,并使用自定义函数决定如何替换 + * + * @param str 要替换的字符串 + * @param regex 用于匹配的正则式 + * @param replaceFun 决定如何替换的函数 + * @return 替换后的字符串 + * @see ReUtil#replaceAll(CharSequence, String, Func1) + * @since 4.2.2 + */ + public static String replace(CharSequence str, String regex, Func1 replaceFun) { + return ReUtil.replaceAll(str, regex, replaceFun); + } + + /** + * 替换指定字符串的指定区间内字符为"*" + * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 替换后的字符串 + * @since 4.1.14 + */ + public static String hide(CharSequence str, int startInclude, int endExclude) { + return replace(str, startInclude, endExclude, '*'); + } + + /** + * 替换字符字符数组中所有的字符为replacedStr
+ * 提供的chars为所有需要被替换的字符,例如:"\r\n",则"\r"和"\n"都会被替换,哪怕他们单独存在 + * + * @param str 被检查的字符串 + * @param chars 需要替换的字符列表,用一个字符串表示这个字符列表 + * @param replacedStr 替换成的字符串 + * @return 新字符串 + * @since 3.2.2 + */ + public static String replaceChars(CharSequence str, String chars, CharSequence replacedStr) { + if (isEmpty(str) || isEmpty(chars)) { + return str(str); + } + return replaceChars(str, chars.toCharArray(), replacedStr); + } + + /** + * 替换字符字符数组中所有的字符为replacedStr + * + * @param str 被检查的字符串 + * @param chars 需要替换的字符列表 + * @param replacedStr 替换成的字符串 + * @return 新字符串 + * @since 3.2.2 + */ + public static String replaceChars(CharSequence str, char[] chars, CharSequence replacedStr) { + if (isEmpty(str) || ArrayUtil.isEmpty(chars)) { + return str(str); + } + + final Set set = new HashSet<>(chars.length); + for (char c : chars) { + set.add(c); + } + int strLen = str.length(); + final StringBuilder builder = new StringBuilder(); + char c; + for (int i = 0; i < strLen; i++) { + c = str.charAt(i); + builder.append(set.contains(c) ? replacedStr : c); + } + return builder.toString(); + } + + // ------------------------------------------------------------------------ length + + /** + * 获取字符串的长度,如果为null返回0 + * + * @param cs a 字符串 + * @return 字符串的长度,如果为null返回0 + * @since 4.3.2 + */ + public static int length(CharSequence cs) { + return cs == null ? 0 : cs.length(); + } + + /** + * 给定字符串转为bytes后的byte数(byte长度) + * + * @param cs 字符串 + * @param charset 编码 + * @return byte长度 + * @since 4.5.2 + */ + public static int byteLength(CharSequence cs, Charset charset) { + return cs == null ? 0 : cs.toString().getBytes(charset).length; + } + + /** + * 给定字符串数组的总长度
+ * null字符长度定义为0 + * + * @param strs 字符串数组 + * @return 总长度 + * @since 4.0.1 + */ + public static int totalLength(CharSequence... strs) { + int totalLength = 0; + for (CharSequence str : strs) { + totalLength += (null == str ? 0 : str.length()); + } + return totalLength; + } + + /** + * 限制字符串长度,如果超过指定长度,截取指定长度并在末尾加"..." + * + * @param string 字符串 + * @param length 最大长度 + * @return 切割后的剩余的前半部分字符串+"..." + * @since 4.0.10 + */ + public static String maxLength(CharSequence string, int length) { + Assert.isTrue(length > 0); + if (null == string) { + return null; + } + if (string.length() <= length) { + return string.toString(); + } + return sub(string, 0, length) + "..."; + } + + // ------------------------------------------------------------------------ firstXXX + + /** + * 返回第一个非{@code null} 元素 + * + * @param strs 多个元素 + * @param 元素类型 + * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} + * @since 5.4.1 + */ + @SuppressWarnings("unchecked") + public T firstNonNull(T... strs) { + return ArrayUtil.firstNonNull(strs); + } + + /** + * 返回第一个非empty 元素 + * + * @param strs 多个元素 + * @param 元素类型 + * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} + * @see #isNotEmpty(CharSequence) + * @since 5.4.1 + */ + @SuppressWarnings("unchecked") + public T firstNonEmpty(T... strs) { + return ArrayUtil.firstMatch(StrUtil::isNotEmpty, strs); + } + + /** + * 返回第一个非blank 元素 + * + * @param strs 多个元素 + * @param 元素类型 + * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} + * @see #isNotBlank(CharSequence) + * @since 5.4.1 + */ + @SuppressWarnings("unchecked") + public T firstNonBlank(T... strs) { + return ArrayUtil.firstMatch(StrUtil::isNotBlank, strs); + } + + // ------------------------------------------------------------------------ lower and upper + + /** + * 原字符串首字母大写并在其首部添加指定字符串 例如:str=name, preString=get =》 return getName + * + * @param str 被处理的字符串 + * @param preString 添加的首部 + * @return 处理后的字符串 + */ + public static String upperFirstAndAddPre(CharSequence str, String preString) { + if (str == null || preString == null) { + return null; + } + return preString + upperFirst(str); + } + + /** + * 大写首字母
+ * 例如:str = name, return Name + * + * @param str 字符串 + * @return 字符串 + */ + public static String upperFirst(CharSequence str) { + if (null == str) { + return null; + } + if (str.length() > 0) { + char firstChar = str.charAt(0); + if (Character.isLowerCase(firstChar)) { + return Character.toUpperCase(firstChar) + subSuf(str, 1); + } + } + return str.toString(); + } + + /** + * 小写首字母
+ * 例如:str = Name, return name + * + * @param str 字符串 + * @return 字符串 + */ + public static String lowerFirst(CharSequence str) { + if (null == str) { + return null; + } + if (str.length() > 0) { + char firstChar = str.charAt(0); + if (Character.isUpperCase(firstChar)) { + return Character.toLowerCase(firstChar) + subSuf(str, 1); + } + } + return str.toString(); + } + + // ------------------------------------------------------------------------ filter + + /** + * 过滤字符串 + * + * @param str 字符串 + * @param filter 过滤器 + * @return 过滤后的字符串 + * @since 5.4.0 + */ + public static String filter(CharSequence str, final Filter filter) { + if (str == null || filter == null) { + return str(str); + } + + int len = str.length(); + final StringBuilder sb = new StringBuilder(len); + char c; + for (int i = 0; i < len; i++) { + c = str.charAt(i); + if (filter.accept(c)) { + sb.append(c); + } + } + return sb.toString(); + } + + // ------------------------------------------------------------------------ case + + /** + * 给定字符串中的字母是否全部为大写,判断依据如下: + * + *
+	 * 1. 大写字母包括A-Z
+	 * 2. 其它非字母的Unicode符都算作大写
+	 * 
+ * + * @param str 被检查的字符串 + * @return 是否全部为大写 + * @since 4.2.2 + */ + public static boolean isUpperCase(CharSequence str) { + if (null == str) { + return false; + } + final int len = str.length(); + for (int i = 0; i < len; i++) { + if (Character.isLowerCase(str.charAt(i))) { + return false; + } + } + return true; + } + + /** + * 给定字符串中的字母是否全部为小写,判断依据如下: + * + *
+	 * 1. 小写字母包括a-z
+	 * 2. 其它非字母的Unicode符都算作小写
+	 * 
+ * + * @param str 被检查的字符串 + * @return 是否全部为小写 + * @since 4.2.2 + */ + public static boolean isLowerCase(CharSequence str) { + if (null == str) { + return false; + } + final int len = str.length(); + for (int i = 0; i < len; i++) { + if (Character.isUpperCase(str.charAt(i))) { + return false; + } + } + return true; + } + + /** + * 切换给定字符串中的大小写。大写转小写,小写转大写。 + * + *
+	 * StrUtil.swapCase(null)                 = null
+	 * StrUtil.swapCase("")                   = ""
+	 * StrUtil.swapCase("The dog has a BONE") = "tHE DOG HAS A bone"
+	 * 
+ * + * @param str 字符串 + * @return 交换后的字符串 + * @since 4.3.2 + */ + public static String swapCase(final String str) { + if (isEmpty(str)) { + return str; + } + + final char[] buffer = str.toCharArray(); + + for (int i = 0; i < buffer.length; i++) { + final char ch = buffer[i]; + if (Character.isUpperCase(ch)) { + buffer[i] = Character.toLowerCase(ch); + } else if (Character.isTitleCase(ch)) { + buffer[i] = Character.toLowerCase(ch); + } else if (Character.isLowerCase(ch)) { + buffer[i] = Character.toUpperCase(ch); + } + } + return new String(buffer); + } + + /** + * 将驼峰式命名的字符串转换为下划线方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。
+ * 例如: + * + *
+	 * HelloWorld=》hello_world
+	 * Hello_World=》hello_world
+	 * HelloWorld_test=》hello_world_test
+	 * 
+ * + * @param str 转换前的驼峰式命名的字符串,也可以为下划线形式 + * @return 转换后下划线方式命名的字符串 + */ + public static String toUnderlineCase(CharSequence str) { + return toSymbolCase(str, CharUtil.UNDERLINE); + } + + /** + * 将驼峰式命名的字符串转换为使用符号连接方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。
+ * + * @param str 转换前的驼峰式命名的字符串,也可以为符号连接形式 + * @param symbol 连接符 + * @return 转换后符号连接方式命名的字符串 + * @since 4.0.10 + */ + public static String toSymbolCase(CharSequence str, char symbol) { + if (str == null) { + return null; + } + + final int length = str.length(); + final StrBuilder sb = new StrBuilder(); + char c; + for (int i = 0; i < length; i++) { + c = str.charAt(i); + final Character preChar = (i > 0) ? str.charAt(i - 1) : null; + if (Character.isUpperCase(c)) { + // 遇到大写字母处理 + final Character nextChar = (i < str.length() - 1) ? str.charAt(i + 1) : null; + if (null != preChar && Character.isUpperCase(preChar)) { + // 前一个字符为大写,则按照一个词对待,例如AB + sb.append(c); + } else if (null != nextChar && (false == Character.isLowerCase(nextChar))) { + // 后一个为非小写字母,按照一个词对待 + if (null != preChar && symbol != preChar) { + // 前一个是非大写时按照新词对待,加连接符,例如xAB + sb.append(symbol); + } + sb.append(c); + } else { + // 前后都为非大写按照新词对待 + if (null != preChar && symbol != preChar) { + // 前一个非连接符,补充连接符 + sb.append(symbol); + } + sb.append(Character.toLowerCase(c)); + } + } else { + if (symbol != c + && sb.length() > 0 + && Character.isUpperCase(sb.charAt(-1)) + && Character.isLowerCase(c)) { + // 当结果中前一个字母为大写,当前为小写(非数字或字符),说明此字符为新词开始(连接符也表示新词) + sb.append(symbol); + } + // 小写或符号 + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 将下划线方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。
+ * 例如:hello_world=》helloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String toCamelCase(CharSequence name) { + if (null == name) { + return null; + } + + final String name2 = name.toString(); + if (contains(name2, CharUtil.UNDERLINE)) { + final int length = name2.length(); + final StringBuilder sb = new StringBuilder(length); + boolean upperCase = false; + for (int i = 0; i < length; i++) { + char c = name2.charAt(i); + + if (c == CharUtil.UNDERLINE) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(Character.toLowerCase(c)); + } + } + return sb.toString(); + } else { + return name2; + } + } + + // ------------------------------------------------------------------------ isSurround + + /** + * 给定字符串是否被字符包围 + * + * @param str 字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 是否包围,空串不包围 + */ + public static boolean isSurround(CharSequence str, CharSequence prefix, CharSequence suffix) { + if (StrUtil.isBlank(str)) { + return false; + } + if (str.length() < (prefix.length() + suffix.length())) { + return false; + } + + final String str2 = str.toString(); + return str2.startsWith(prefix.toString()) && str2.endsWith(suffix.toString()); + } + + /** + * 给定字符串是否被字符包围 + * + * @param str 字符串 + * @param prefix 前缀 + * @param suffix 后缀 + * @return 是否包围,空串不包围 + */ + public static boolean isSurround(CharSequence str, char prefix, char suffix) { + if (StrUtil.isBlank(str)) { + return false; + } + if (str.length() < 2) { + return false; + } + + return str.charAt(0) == prefix && str.charAt(str.length() - 1) == suffix; + } + + // ------------------------------------------------------------------------ builder + + /** + * 创建StringBuilder对象 + * + * @param strs 初始字符串列表 + * @return StringBuilder对象 + */ + public static StringBuilder builder(CharSequence... strs) { + final StringBuilder sb = new StringBuilder(); + for (CharSequence str : strs) { + sb.append(str); + } + return sb; + } + + /** + * 创建StrBuilder对象 + * + * @param strs 初始字符串列表 + * @return StrBuilder对象 + */ + public static StrBuilder strBuilder(CharSequence... strs) { + return StrBuilder.create(strs); + } + + // ------------------------------------------------------------------------ getter and setter + + /** + * 获得set或get或is方法对应的标准属性名
+ * 例如:setName 返回 name + * + *
+	 * getName =》name
+	 * setName =》name
+	 * isName  =》name
+	 * 
+ * + * @param getOrSetMethodName Get或Set方法名 + * @return 如果是set或get方法名,返回field, 否则null + */ + public static String getGeneralField(CharSequence getOrSetMethodName) { + final String getOrSetMethodNameStr = getOrSetMethodName.toString(); + if (getOrSetMethodNameStr.startsWith("get") || getOrSetMethodNameStr.startsWith("set")) { + return removePreAndLowerFirst(getOrSetMethodName, 3); + } else if (getOrSetMethodNameStr.startsWith("is")) { + return removePreAndLowerFirst(getOrSetMethodName, 2); + } + return null; + } + + /** + * 生成set方法名
+ * 例如:name 返回 setName + * + * @param fieldName 属性名 + * @return setXxx + */ + public static String genSetter(CharSequence fieldName) { + return upperFirstAndAddPre(fieldName, "set"); + } + + /** + * 生成get方法名 + * + * @param fieldName 属性名 + * @return getXxx + */ + public static String genGetter(CharSequence fieldName) { + return upperFirstAndAddPre(fieldName, "get"); + } + + // ------------------------------------------------------------------------ other + + /** + * 连接多个字符串为一个 + * + * @param isNullToEmpty 是否null转为"" + * @param strs 字符串数组 + * @return 连接后的字符串 + * @since 4.1.0 + */ + public static String concat(boolean isNullToEmpty, CharSequence... strs) { + final StrBuilder sb = new StrBuilder(); + for (CharSequence str : strs) { + sb.append(isNullToEmpty ? nullToEmpty(str) : str); + } + return sb.toString(); + } + + /** + * 将给定字符串,变成 "xxx...xxx" 形式的字符串 + * + * @param str 字符串 + * @param maxLength 最大长度 + * @return 截取后的字符串 + */ + public static String brief(CharSequence str, int maxLength) { + if (null == str) { + return null; + } + if (str.length() <= maxLength) { + return str.toString(); + } + int w = maxLength / 2; + int l = str.length() + 3; + + final String str2 = str.toString(); + return format("{}...{}", str2.substring(0, maxLength - w), str2.substring(l - w)); + } + + /** + * 以 conjunction 为分隔符将多个对象转换为字符串 + * + * @param conjunction 分隔符 + * @param objs 数组 + * @return 连接后的字符串 + * @see ArrayUtil#join(Object, CharSequence) + */ + public static String join(CharSequence conjunction, Object... objs) { + return ArrayUtil.join(objs, conjunction); + } + + /** + * 字符串的每一个字符是否都与定义的匹配器匹配 + * + * @param value 字符串 + * @param matcher 匹配器 + * @return 是否全部匹配 + * @since 3.2.3 + */ + public static boolean isAllCharMatch(CharSequence value, cn.hutool.core.lang.Matcher matcher) { + if (StrUtil.isBlank(value)) { + return false; + } + int len = value.length(); + for (int i = 0; i < len; i++) { + if (false == matcher.match(value.charAt(i))) { + return false; + } + } + return true; + } + + /** + * 循环位移指定位置的字符串为指定距离
+ * 当moveLength大于0向右位移,小于0向左位移,0不位移
+ * 当moveLength大于字符串长度时采取循环位移策略,即位移到头后从头(尾)位移,例如长度为10,位移13则表示位移3 + * + * @param str 字符串 + * @param startInclude 起始位置(包括) + * @param endExclude 结束位置(不包括) + * @param moveLength 移动距离,负数表示左移,正数为右移 + * @return 位移后的字符串 + * @since 4.0.7 + */ + public static String move(CharSequence str, int startInclude, int endExclude, int moveLength) { + if (isEmpty(str)) { + return str(str); + } + int len = str.length(); + if (Math.abs(moveLength) > len) { + // 循环位移,当越界时循环 + moveLength = moveLength % len; + } + final StrBuilder strBuilder = StrBuilder.create(len); + if (moveLength > 0) { + int endAfterMove = Math.min(endExclude + moveLength, str.length()); + strBuilder.append(str.subSequence(0, startInclude))// + .append(str.subSequence(endExclude, endAfterMove))// + .append(str.subSequence(startInclude, endExclude))// + .append(str.subSequence(endAfterMove, str.length())); + } else if (moveLength < 0) { + int startAfterMove = Math.max(startInclude + moveLength, 0); + strBuilder.append(str.subSequence(0, startAfterMove))// + .append(str.subSequence(startInclude, endExclude))// + .append(str.subSequence(startAfterMove, startInclude))// + .append(str.subSequence(endExclude, str.length())); + } else { + return str(str); + } + return strBuilder.toString(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java index 868aeca3f..a9b684eea 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java @@ -1,38 +1,21 @@ package cn.hutool.core.util; -import cn.hutool.core.comparator.VersionComparator; -import cn.hutool.core.convert.Convert; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.lang.Filter; -import cn.hutool.core.lang.Matcher; -import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.StrBuilder; -import cn.hutool.core.text.StrFormatter; -import cn.hutool.core.text.StrSpliter; import cn.hutool.core.text.TextSimilarity; import java.io.StringReader; import java.io.StringWriter; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.regex.Pattern; /** * 字符串工具类 * * @author xiaoleilu */ -public class StrUtil { - - public static final int INDEX_NOT_FOUND = -1; +public class StrUtil extends CharSequenceUtil { /** * 字符常量:空格符 {@code ' '} @@ -110,11 +93,6 @@ public class StrUtil { public static final char C_AT = CharUtil.AT; - /** - * 字符串常量:空格符 {@code " "} - */ - public static final String SPACE = " "; - /** * 字符串常量:制表符 {@code "\t"} */ @@ -141,17 +119,6 @@ public class StrUtil { */ public static final String BACKSLASH = "\\"; - /** - * 字符串常量:空字符串 {@code ""} - */ - public static final String EMPTY = ""; - - /** - * 字符串常量:{@code "null"}
- * 注意:{@code "null" != null} - */ - public static final String NULL = "null"; - /** * 字符串常量:回车符 {@code "\r"}
* 解释:该字符常用于表示 Linux 系统和 MacOS 系统下的文本换行 @@ -218,88 +185,40 @@ public class StrUtil { /** * 字符串常量:HTML 空格转义 {@code " " -> " "} */ - public static final String HTML_NBSP = " "; + public static final String HTML_NBSP = XmlUtil.NBSP; /** * 字符串常量:HTML And 符转义 {@code "&" -> "&"} */ - public static final String HTML_AMP = "&"; + public static final String HTML_AMP = XmlUtil.AMP; /** * 字符串常量:HTML 双引号转义 {@code """ -> "\""} */ - public static final String HTML_QUOTE = """; + public static final String HTML_QUOTE = XmlUtil.QUOTE; /** * 字符串常量:HTML 单引号转义 {@code "&apos" -> "'"} */ - public static final String HTML_APOS = "'"; + public static final String HTML_APOS = XmlUtil.APOS; /** * 字符串常量:HTML 小于号转义 {@code "<" -> "<"} */ - public static final String HTML_LT = "<"; + public static final String HTML_LT = XmlUtil.LT; /** * 字符串常量:HTML 大于号转义 {@code ">" -> ">"} */ - public static final String HTML_GT = ">"; + public static final String HTML_GT = XmlUtil.GT; /** * 字符串常量:空 JSON {@code "{}"} */ public static final String EMPTY_JSON = "{}"; - // ------------------------------------------------------------------------ Blank - /** - *

字符串是否为空白,空白的定义如下:

- *
    - *
  1. {@code null}
  2. - *
  3. 空字符串:{@code ""}
  4. - *
  5. 空格、全角空格、制表符、换行符,等不可见字符
  6. - *
- * - *

例:

- *
    - *
  • {@code StrUtil.isBlank(null) // true}
  • - *
  • {@code StrUtil.isBlank("") // true}
  • - *
  • {@code StrUtil.isBlank(" \t\n") // true}
  • - *
  • {@code StrUtil.isBlank("abc") // false}
  • - *
- * - *

注意:该方法与 {@link #isEmpty(CharSequence)} 的区别是: - * 该方法会校验空白字符,且性能相对于 {@link #isEmpty(CharSequence)} 略慢。

- *
- * - *

建议:

- *
    - *
  • 该方法建议仅对于客户端(或第三方接口)传入的参数使用该方法。
  • - *
  • 需要同时校验多个字符串时,建议采用 {@link #hasBlank(CharSequence...)} 或 {@link #isAllBlank(CharSequence...)}
  • - *
- * - * @param str 被检测的字符串 - * @return 若为空白,则返回 true - * @see #isEmpty(CharSequence) - */ - public static boolean isBlank(CharSequence str) { - int length; - - if ((str == null) || ((length = str.length()) == 0)) { - return true; - } - - for (int i = 0; i < length; i++) { - // 只要有一个非空字符即为非空字符串 - if (false == CharUtil.isBlankChar(str.charAt(i))) { - return false; - } - } - - return true; - } - /** *

如果对象是字符串是否为空白,空白的定义如下:

*
    @@ -332,137 +251,8 @@ public class StrUtil { } return false; } - - /** - *

    字符串是否为非空白,非空白的定义如下:

    - *
      - *
    1. 不为 {@code null}
    2. - *
    3. 不为空字符串:{@code ""}
    4. - *
    5. 不为空格、全角空格、制表符、换行符,等不可见字符
    6. - *
    - * - *

    例:

    - *
      - *
    • {@code StrUtil.isNotBlank(null) // false}
    • - *
    • {@code StrUtil.isNotBlank("") // false}
    • - *
    • {@code StrUtil.isNotBlank(" \t\n") // false}
    • - *
    • {@code StrUtil.isNotBlank("abc") // true}
    • - *
    - * - *

    注意:该方法与 {@link #isNotEmpty(CharSequence)} 的区别是: - * 该方法会校验空白字符,且性能相对于 {@link #isNotEmpty(CharSequence)} 略慢。

    - *

    建议:仅对于客户端(或第三方接口)传入的参数使用该方法。

    - * - * @param str 被检测的字符串 - * @return 是否为非空 - * @see StrUtil#isBlank(CharSequence) - */ - public static boolean isNotBlank(CharSequence str) { - return false == isBlank(str); - } - - /** - *

    指定字符串数组中,是否包含空字符串。

    - *

    如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。

    - *
    - * - *

    例:

    - *
      - *
    • {@code StrUtil.hasBlank() // true}
    • - *
    • {@code StrUtil.hasBlank("", null, " ") // true}
    • - *
    • {@code StrUtil.hasBlank("123", " ") // true}
    • - *
    • {@code StrUtil.hasBlank("123", "abc") // false}
    • - *
    - * - *

    注意:该方法与 {@link #isAllBlank(CharSequence...)} 的区别在于:

    - *
      - *
    • hasBlank(CharSequence...) 等价于 {@code isBlank(...) || isBlank(...) || ...}
    • - *
    • {@link #isAllBlank(CharSequence...)} 等价于 {@code isBlank(...) && isBlank(...) && ...}
    • - *
    - * - * @param strs 字符串列表 - * @return 是否包含空字符串 - */ - public static boolean hasBlank(CharSequence... strs) { - if (ArrayUtil.isEmpty(strs)) { - return true; - } - - for (CharSequence str : strs) { - if (isBlank(str)) { - return true; - } - } - return false; - } - - /** - *

    指定字符串数组中的元素,是否全部为空字符串。

    - *

    如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。

    - *
    - * - *

    例:

    - *
      - *
    • {@code StrUtil.isAllBlank() // true}
    • - *
    • {@code StrUtil.isAllBlank("", null, " ") // true}
    • - *
    • {@code StrUtil.isAllBlank("123", " ") // false}
    • - *
    • {@code StrUtil.isAllBlank("123", "abc") // false}
    • - *
    - * - *

    注意:该方法与 {@link #hasBlank(CharSequence...)} 的区别在于:

    - *
      - *
    • {@link #hasBlank(CharSequence...)} 等价于 {@code isBlank(...) || isBlank(...) || ...}
    • - *
    • isAllBlank(CharSequence...) 等价于 {@code isBlank(...) && isBlank(...) && ...}
    • - *
    - * - * @param strs 字符串列表 - * @return 所有字符串是否为空白 - */ - public static boolean isAllBlank(CharSequence... strs) { - if (ArrayUtil.isEmpty(strs)) { - return true; - } - - for (CharSequence str : strs) { - if (isNotBlank(str)) { - return false; - } - } - return true; - } - // ------------------------------------------------------------------------ Empty - /** - *

    字符串是否为空,空的定义如下:

    - *
      - *
    1. {@code null}
    2. - *
    3. 空字符串:{@code ""}
    4. - *
    - * - *

    例:

    - *
      - *
    • {@code StrUtil.isEmpty(null) // true}
    • - *
    • {@code StrUtil.isEmpty("") // true}
    • - *
    • {@code StrUtil.isEmpty(" \t\n") // false}
    • - *
    • {@code StrUtil.isEmpty("abc") // false}
    • - *
    - * - *

    注意:该方法与 {@link #isBlank(CharSequence)} 的区别是:该方法不校验空白字符。

    - *

    建议:

    - *
      - *
    • 该方法建议用于工具类或任何可以预期的方法参数的校验中。
    • - *
    • 需要同时校验多个字符串时,建议采用 {@link #hasEmpty(CharSequence...)} 或 {@link #isAllEmpty(CharSequence...)}
    • - *
    - * - * @param str 被检测的字符串 - * @return 是否为空 - * @see #isBlank(CharSequence) - */ - public static boolean isEmpty(CharSequence str) { - return str == null || str.length() == 0; - } - /** *

    如果对象是字符串是否为空串,空的定义如下:


    *
      @@ -493,307 +283,8 @@ public class StrUtil { return false; } - /** - *

      字符串是否为非空白,非空白的定义如下:

      - *
        - *
      1. 不为 {@code null}
      2. - *
      3. 不为空字符串:{@code ""}
      4. - *
      - * - *

      例:

      - *
        - *
      • {@code StrUtil.isNotEmpty(null) // false}
      • - *
      • {@code StrUtil.isNotEmpty("") // false}
      • - *
      • {@code StrUtil.isNotEmpty(" \t\n") // true}
      • - *
      • {@code StrUtil.isNotEmpty("abc") // true}
      • - *
      - * - *

      注意:该方法与 {@link #isNotBlank(CharSequence)} 的区别是:该方法不校验空白字符。

      - *

      建议:该方法建议用于工具类或任何可以预期的方法参数的校验中。

      - * - * @param str 被检测的字符串 - * @return 是否为非空 - * @see StrUtil#isEmpty(CharSequence) - */ - public static boolean isNotEmpty(CharSequence str) { - return false == isEmpty(str); - } - - /** - * 当给定字符串为null时,转换为Empty - * - * @param str 被检查的字符串 - * @return 原字符串或者空串 - * @see #nullToEmpty(CharSequence) - * @since 4.6.3 - */ - public static String emptyIfNull(CharSequence str) { - return nullToEmpty(str); - } - - /** - * 当给定字符串为null时,转换为Empty - * - * @param str 被转换的字符串 - * @return 转换后的字符串 - */ - public static String nullToEmpty(CharSequence str) { - return nullToDefault(str, EMPTY); - } - - /** - * 如果字符串是 {@code null},则返回指定默认字符串,否则返回字符串本身。 - * - *
      -	 * nullToDefault(null, "default")  = "default"
      -	 * nullToDefault("", "default")    = ""
      -	 * nullToDefault("  ", "default")  = "  "
      -	 * nullToDefault("bat", "default") = "bat"
      -	 * 
      - * - * @param str 要转换的字符串 - * @param defaultStr 默认字符串 - * @return 字符串本身或指定的默认字符串 - */ - public static String nullToDefault(CharSequence str, String defaultStr) { - return (str == null) ? defaultStr : str.toString(); - } - - /** - * 如果字符串是{@code null}或者"",则返回指定默认字符串,否则返回字符串本身。 - * - *
      -	 * emptyToDefault(null, "default")  = "default"
      -	 * emptyToDefault("", "default")    = "default"
      -	 * emptyToDefault("  ", "default")  = "  "
      -	 * emptyToDefault("bat", "default") = "bat"
      -	 * 
      - * - * @param str 要转换的字符串 - * @param defaultStr 默认字符串 - * @return 字符串本身或指定的默认字符串 - * @since 4.1.0 - */ - public static String emptyToDefault(CharSequence str, String defaultStr) { - return isEmpty(str) ? defaultStr : str.toString(); - } - - /** - * 如果字符串是{@code null}或者""或者空白,则返回指定默认字符串,否则返回字符串本身。 - * - *
      -	 * emptyToDefault(null, "default")  = "default"
      -	 * emptyToDefault("", "default")    = "default"
      -	 * emptyToDefault("  ", "default")  = "default"
      -	 * emptyToDefault("bat", "default") = "bat"
      -	 * 
      - * - * @param str 要转换的字符串 - * @param defaultStr 默认字符串 - * @return 字符串本身或指定的默认字符串 - * @since 4.1.0 - */ - public static String blankToDefault(CharSequence str, String defaultStr) { - return isBlank(str) ? defaultStr : str.toString(); - } - - /** - * 当给定字符串为空字符串时,转换为{@code null} - * - * @param str 被转换的字符串 - * @return 转换后的字符串 - */ - public static String emptyToNull(CharSequence str) { - return isEmpty(str) ? null : str.toString(); - } - - /** - *

      是否包含空字符串。

      - *

      如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。

      - *
      - * - *

      例:

      - *
        - *
      • {@code StrUtil.hasEmpty() // true}
      • - *
      • {@code StrUtil.hasEmpty("", null) // true}
      • - *
      • {@code StrUtil.hasEmpty("123", "") // true}
      • - *
      • {@code StrUtil.hasEmpty("123", "abc") // false}
      • - *
      • {@code StrUtil.hasEmpty(" ", "\t", "\n") // false}
      • - *
      - * - *

      注意:该方法与 {@link #isAllEmpty(CharSequence...)} 的区别在于:

      - *
        - *
      • hasEmpty(CharSequence...) 等价于 {@code isEmpty(...) || isEmpty(...) || ...}
      • - *
      • {@link #isAllEmpty(CharSequence...)} 等价于 {@code isEmpty(...) && isEmpty(...) && ...}
      • - *
      - * - * @param strs 字符串列表 - * @return 是否包含空字符串 - */ - public static boolean hasEmpty(CharSequence... strs) { - if (ArrayUtil.isEmpty(strs)) { - return true; - } - - for (CharSequence str : strs) { - if (isEmpty(str)) { - return true; - } - } - return false; - } - - /** - *

      指定字符串数组中的元素,是否全部为空字符串。

      - *

      如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。

      - *
      - * - *

      例:

      - *
        - *
      • {@code StrUtil.isAllEmpty() // true}
      • - *
      • {@code StrUtil.isAllEmpty("", null) // true}
      • - *
      • {@code StrUtil.isAllEmpty("123", "") // false}
      • - *
      • {@code StrUtil.isAllEmpty("123", "abc") // false}
      • - *
      • {@code StrUtil.isAllEmpty(" ", "\t", "\n") // false}
      • - *
      - * - *

      注意:该方法与 {@link #hasEmpty(CharSequence...)} 的区别在于:

      - *
        - *
      • {@link #hasEmpty(CharSequence...)} 等价于 {@code isEmpty(...) || isEmpty(...) || ...}
      • - *
      • isAllEmpty(CharSequence...) 等价于 {@code isEmpty(...) && isEmpty(...) && ...}
      • - *
      - * - * @param strs 字符串列表 - * @return 所有字符串是否为空白 - */ - public static boolean isAllEmpty(CharSequence... strs) { - if (ArrayUtil.isEmpty(strs)) { - return true; - } - - for (CharSequence str : strs) { - if (isNotEmpty(str)) { - return false; - } - } - return true; - } - - /** - *

      指定字符串数组中的元素,是否都不为空字符串。

      - *

      如果指定的字符串数组的长度不为 0,或者所有元素都不是空字符串,则返回 true。

      - *
      - * - *

      例:

      - *
        - *
      • {@code StrUtil.isAllNotEmpty() // false}
      • - *
      • {@code StrUtil.isAllNotEmpty("", null) // false}
      • - *
      • {@code StrUtil.isAllNotEmpty("123", "") // false}
      • - *
      • {@code StrUtil.isAllNotEmpty("123", "abc") // true}
      • - *
      • {@code StrUtil.isAllNotEmpty(" ", "\t", "\n") // true}
      • - *
      - * - *

      注意:该方法与 {@link #isAllEmpty(CharSequence...)} 的区别在于:

      - *
        - *
      • {@link #isAllEmpty(CharSequence...)} 等价于 {@code isEmpty(...) && isEmpty(...) && ...}
      • - *
      • isAllNotEmpty(CharSequence...) 等价于 {@code !isEmpty(...) && !isEmpty(...) && ...}
      • - *
      - * - * @param args 字符串数组 - * @return 所有字符串是否都不为为空白 - * @since 5.3.6 - */ - public static boolean isAllNotEmpty(CharSequence... args) { - return false == hasEmpty(args); - } - - /** - * 是否存都不为{@code null}或空对象或空白符的对象,通过{@link StrUtil#hasBlank(CharSequence...)} 判断元素 - * - * @param args 被检查的对象,一个或者多个 - * @return 是否都不为空 - * @since 5.3.6 - */ - public static boolean isAllNotBlank(CharSequence... args) { - return false == hasBlank(args); - } - - /** - * 检查字符串是否为null、“null”、“undefined” - * - * @param str 被检查的字符串 - * @return 是否为null、“null”、“undefined” - * @since 4.0.10 - */ - public static boolean isNullOrUndefined(CharSequence str) { - if (null == str) { - return true; - } - return isNullOrUndefinedStr(str); - } - - /** - * 检查字符串是否为null、“”、“null”、“undefined” - * - * @param str 被检查的字符串 - * @return 是否为null、“”、“null”、“undefined” - * @since 4.0.10 - */ - public static boolean isEmptyOrUndefined(CharSequence str) { - if (isEmpty(str)) { - return true; - } - return isNullOrUndefinedStr(str); - } - - /** - * 检查字符串是否为null、空白串、“null”、“undefined” - * - * @param str 被检查的字符串 - * @return 是否为null、空白串、“null”、“undefined” - * @since 4.0.10 - */ - public static boolean isBlankOrUndefined(CharSequence str) { - if (isBlank(str)) { - return true; - } - return isNullOrUndefinedStr(str); - } - - /** - * 是否为“null”、“undefined”,不做空指针检查 - * - * @param str 字符串 - * @return 是否为“null”、“undefined” - */ - private static boolean isNullOrUndefinedStr(CharSequence str) { - String strString = str.toString().trim(); - return NULL.equals(strString) || "undefined".equals(strString); - } - // ------------------------------------------------------------------------ Trim - /** - * 除去字符串头尾部的空白,如果字符串是{@code null},依然返回{@code null}。 - * - *

      - * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 - * - *

      -	 * trim(null)          = null
      -	 * trim("")            = ""
      -	 * trim("     ")       = ""
      -	 * trim("abc")         = "abc"
      -	 * trim("    abc    ") = "abc"
      -	 * 
      - * - * @param str 要处理的字符串 - * @return 除去头尾空白的字符串,如果原字串为{@code null},则返回{@code null} - */ - public static String trim(CharSequence str) { - return (null == str) ? null : trim(str, 0); - } - /** * 给定字符串数组全部做去首尾空格 * @@ -807,2044 +298,11 @@ public class StrUtil { for (int i = 0; i < strs.length; i++) { str = strs[i]; if (null != str) { - strs[i] = str.trim(); + strs[i] = trim(str); } } } - /** - * 除去字符串头尾部的空白,如果字符串是{@code null},返回{@code ""}。 - * - *
      -	 * StrUtil.trimToEmpty(null)          = ""
      -	 * StrUtil.trimToEmpty("")            = ""
      -	 * StrUtil.trimToEmpty("     ")       = ""
      -	 * StrUtil.trimToEmpty("abc")         = "abc"
      -	 * StrUtil.trimToEmpty("    abc    ") = "abc"
      -	 * 
      - * - * @param str 字符串 - * @return 去除两边空白符后的字符串, 如果为null返回"" - * @since 3.1.1 - */ - public static String trimToEmpty(CharSequence str) { - return str == null ? EMPTY : trim(str); - } - - /** - * 除去字符串头尾部的空白,如果字符串是{@code null}或者"",返回{@code null}。 - * - *
      -	 * StrUtil.trimToNull(null)          = null
      -	 * StrUtil.trimToNull("")            = null
      -	 * StrUtil.trimToNull("     ")       = null
      -	 * StrUtil.trimToNull("abc")         = "abc"
      -	 * StrUtil.trimToEmpty("    abc    ") = "abc"
      -	 * 
      - * - * @param str 字符串 - * @return 去除两边空白符后的字符串, 如果为空返回null - * @since 3.2.1 - */ - public static String trimToNull(CharSequence str) { - final String trimStr = trim(str); - return EMPTY.equals(trimStr) ? null : trimStr; - } - - /** - * 除去字符串头部的空白,如果字符串是{@code null},则返回{@code null}。 - * - *

      - * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 - * - *

      -	 * trimStart(null)         = null
      -	 * trimStart("")           = ""
      -	 * trimStart("abc")        = "abc"
      -	 * trimStart("  abc")      = "abc"
      -	 * trimStart("abc  ")      = "abc  "
      -	 * trimStart(" abc ")      = "abc "
      -	 * 
      - * - * @param str 要处理的字符串 - * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} - */ - public static String trimStart(CharSequence str) { - return trim(str, -1); - } - - /** - * 除去字符串尾部的空白,如果字符串是{@code null},则返回{@code null}。 - * - *

      - * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 - * - *

      -	 * trimEnd(null)       = null
      -	 * trimEnd("")         = ""
      -	 * trimEnd("abc")      = "abc"
      -	 * trimEnd("  abc")    = "  abc"
      -	 * trimEnd("abc  ")    = "abc"
      -	 * trimEnd(" abc ")    = " abc"
      -	 * 
      - * - * @param str 要处理的字符串 - * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} - */ - public static String trimEnd(CharSequence str) { - return trim(str, 1); - } - - /** - * 除去字符串头尾部的空白符,如果字符串是{@code null},依然返回{@code null}。 - * - * @param str 要处理的字符串 - * @param mode {@code -1}表示trimStart,{@code 0}表示trim全部, {@code 1}表示trimEnd - * @return 除去指定字符后的的字符串,如果原字串为{@code null},则返回{@code null} - */ - public static String trim(CharSequence str, int mode) { - String result; - if (str == null) { - result = null; - } else { - int length = str.length(); - int start = 0; - int end = length;// 扫描字符串头部 - if (mode <= 0) { - while ((start < end) && (CharUtil.isBlankChar(str.charAt(start)))) { - start++; - } - }// 扫描字符串尾部 - if (mode >= 0) { - while ((start < end) && (CharUtil.isBlankChar(str.charAt(end - 1)))) { - end--; - } - } - if ((start > 0) || (end < length)) { - result = str.toString().substring(start, end); - } else { - result = str.toString(); - } - } - - return result; - } - - /** - * 字符串是否以给定字符开始 - * - * @param str 字符串 - * @param c 字符 - * @return 是否开始 - */ - public static boolean startWith(CharSequence str, char c) { - if (isEmpty(str)) { - return false; - } - return c == str.charAt(0); - } - - /** - * 是否以指定字符串开头
      - * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false - * - * @param str 被监测字符串 - * @param prefix 开头字符串 - * @param ignoreCase 是否忽略大小写 - * @return 是否以指定字符串开头 - * @since 5.4.3 - */ - public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase) { - return startWith(str, prefix, ignoreCase, false); - } - - /** - * 是否以指定字符串开头
      - * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false - * - * @param str 被监测字符串 - * @param prefix 开头字符串 - * @param ignoreCase 是否忽略大小写 - * @param ignoreEquals 是否忽略字符串相等的情况 - * @return 是否以指定字符串开头 - * @since 5.4.3 - */ - public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase, boolean ignoreEquals) { - if (null == str || null == prefix) { - if (false == ignoreEquals) { - return false; - } - return null == str && null == prefix; - } - - boolean isStartWith; - if (ignoreCase) { - isStartWith = str.toString().toLowerCase().startsWith(prefix.toString().toLowerCase()); - } else { - isStartWith = str.toString().startsWith(prefix.toString()); - } - - if (isStartWith) { - return (false == ignoreEquals) || (false == equals(str, prefix, ignoreCase)); - } - return false; - } - - /** - * 是否以指定字符串开头 - * - * @param str 被监测字符串 - * @param prefix 开头字符串 - * @return 是否以指定字符串开头 - */ - public static boolean startWith(CharSequence str, CharSequence prefix) { - return startWith(str, prefix, false); - } - - /** - * 是否以指定字符串开头,忽略相等字符串的情况 - * - * @param str 被监测字符串 - * @param prefix 开头字符串 - * @return 是否以指定字符串开头并且两个字符串不相等 - */ - public static boolean startWithIgnoreEquals(CharSequence str, CharSequence prefix) { - return startWith(str, prefix, false, true); - } - - /** - * 是否以指定字符串开头,忽略大小写 - * - * @param str 被监测字符串 - * @param prefix 开头字符串 - * @return 是否以指定字符串开头 - */ - public static boolean startWithIgnoreCase(CharSequence str, CharSequence prefix) { - return startWith(str, prefix, true); - } - - /** - * 给定字符串是否以任何一个字符串开始
      - * 给定字符串和数组为空都返回false - * - * @param str 给定字符串 - * @param prefixes 需要检测的开始字符串 - * @return 给定字符串是否以任何一个字符串开始 - * @since 3.0.6 - */ - public static boolean startWithAny(CharSequence str, CharSequence... prefixes) { - if (isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { - return false; - } - - for (CharSequence suffix : prefixes) { - if (startWith(str, suffix, false)) { - return true; - } - } - return false; - } - - /** - * 字符串是否以给定字符结尾 - * - * @param str 字符串 - * @param c 字符 - * @return 是否结尾 - */ - public static boolean endWith(CharSequence str, char c) { - if (isEmpty(str)) { - return false; - } - return c == str.charAt(str.length() - 1); - } - - /** - * 是否以指定字符串结尾
      - * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false - * - * @param str 被监测字符串 - * @param suffix 结尾字符串 - * @param isIgnoreCase 是否忽略大小写 - * @return 是否以指定字符串结尾 - */ - public static boolean endWith(CharSequence str, CharSequence suffix, boolean isIgnoreCase) { - if (null == str || null == suffix) { - return null == str && null == suffix; - } - - if (isIgnoreCase) { - return str.toString().toLowerCase().endsWith(suffix.toString().toLowerCase()); - } else { - return str.toString().endsWith(suffix.toString()); - } - } - - /** - * 是否以指定字符串结尾 - * - * @param str 被监测字符串 - * @param suffix 结尾字符串 - * @return 是否以指定字符串结尾 - */ - public static boolean endWith(CharSequence str, CharSequence suffix) { - return endWith(str, suffix, false); - } - - /** - * 是否以指定字符串结尾,忽略大小写 - * - * @param str 被监测字符串 - * @param suffix 结尾字符串 - * @return 是否以指定字符串结尾 - */ - public static boolean endWithIgnoreCase(CharSequence str, CharSequence suffix) { - return endWith(str, suffix, true); - } - - /** - * 给定字符串是否以任何一个字符串结尾
      - * 给定字符串和数组为空都返回false - * - * @param str 给定字符串 - * @param suffixes 需要检测的结尾字符串 - * @return 给定字符串是否以任何一个字符串结尾 - * @since 3.0.6 - */ - public static boolean endWithAny(CharSequence str, CharSequence... suffixes) { - if (isEmpty(str) || ArrayUtil.isEmpty(suffixes)) { - return false; - } - - for (CharSequence suffix : suffixes) { - if (endWith(str, suffix, false)) { - return true; - } - } - return false; - } - - /** - * 指定字符是否在字符串中出现过 - * - * @param str 字符串 - * @param searchChar 被查找的字符 - * @return 是否包含 - * @since 3.1.2 - */ - public static boolean contains(CharSequence str, char searchChar) { - return indexOf(str, searchChar) > -1; - } - - /** - * 指定字符串是否在字符串中出现过 - * - * @param str 字符串 - * @param searchStr 被查找的字符串 - * @return 是否包含 - * @since 5.1.1 - */ - public static boolean contains(CharSequence str, CharSequence searchStr) { - if (null == str || null == searchStr) { - return false; - } - return str.toString().contains(searchStr); - } - - /** - * 查找指定字符串是否包含指定字符串列表中的任意一个字符串 - * - * @param str 指定字符串 - * @param testStrs 需要检查的字符串数组 - * @return 是否包含任意一个字符串 - * @since 3.2.0 - */ - public static boolean containsAny(CharSequence str, CharSequence... testStrs) { - return null != getContainsStr(str, testStrs); - } - - /** - * 查找指定字符串是否包含指定字符列表中的任意一个字符 - * - * @param str 指定字符串 - * @param testChars 需要检查的字符数组 - * @return 是否包含任意一个字符 - * @since 4.1.11 - */ - public static boolean containsAny(CharSequence str, char... testChars) { - if (false == isEmpty(str)) { - int len = str.length(); - for (int i = 0; i < len; i++) { - if (ArrayUtil.contains(testChars, str.charAt(i))) { - return true; - } - } - } - return false; - } - - /** - * 检查指定字符串中是否只包含给定的字符 - * - * @param str 字符串 - * @param testChars 检查的字符 - * @return 字符串含有非检查的字符,返回false - * @since 4.4.1 - */ - public static boolean containsOnly(CharSequence str, char... testChars) { - if (false == isEmpty(str)) { - int len = str.length(); - for (int i = 0; i < len; i++) { - if (false == ArrayUtil.contains(testChars, str.charAt(i))) { - return false; - } - } - } - return true; - } - - /** - * 给定字符串是否包含空白符(空白符包括空格、制表符、全角空格和不间断空格)
      - * 如果给定字符串为null或者"",则返回false - * - * @param str 字符串 - * @return 是否包含空白符 - * @since 4.0.8 - */ - public static boolean containsBlank(CharSequence str) { - if (null == str) { - return false; - } - final int length = str.length(); - if (0 == length) { - return false; - } - - for (int i = 0; i < length; i += 1) { - if (CharUtil.isBlankChar(str.charAt(i))) { - return true; - } - } - return false; - } - - /** - * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串 - * - * @param str 指定字符串 - * @param testStrs 需要检查的字符串数组 - * @return 被包含的第一个字符串 - * @since 3.2.0 - */ - public static String getContainsStr(CharSequence str, CharSequence... testStrs) { - if (isEmpty(str) || ArrayUtil.isEmpty(testStrs)) { - return null; - } - for (CharSequence checkStr : testStrs) { - if (str.toString().contains(checkStr)) { - return checkStr.toString(); - } - } - return null; - } - - /** - * 是否包含特定字符,忽略大小写,如果给定两个参数都为{@code null},返回true - * - * @param str 被检测字符串 - * @param testStr 被测试是否包含的字符串 - * @return 是否包含 - */ - public static boolean containsIgnoreCase(CharSequence str, CharSequence testStr) { - if (null == str) { - // 如果被监测字符串和 - return null == testStr; - } - return str.toString().toLowerCase().contains(testStr.toString().toLowerCase()); - } - - /** - * 查找指定字符串是否包含指定字符串列表中的任意一个字符串
      - * 忽略大小写 - * - * @param str 指定字符串 - * @param testStrs 需要检查的字符串数组 - * @return 是否包含任意一个字符串 - * @since 3.2.0 - */ - public static boolean containsAnyIgnoreCase(CharSequence str, CharSequence... testStrs) { - return null != getContainsStrIgnoreCase(str, testStrs); - } - - /** - * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串
      - * 忽略大小写 - * - * @param str 指定字符串 - * @param testStrs 需要检查的字符串数组 - * @return 被包含的第一个字符串 - * @since 3.2.0 - */ - public static String getContainsStrIgnoreCase(CharSequence str, CharSequence... testStrs) { - if (isEmpty(str) || ArrayUtil.isEmpty(testStrs)) { - return null; - } - for (CharSequence testStr : testStrs) { - if (containsIgnoreCase(str, testStr)) { - return testStr.toString(); - } - } - return null; - } - - /** - * 获得set或get或is方法对应的标准属性名
      - * 例如:setName 返回 name - * - *
      -	 * getName =》name
      -	 * setName =》name
      -	 * isName  =》name
      -	 * 
      - * - * @param getOrSetMethodName Get或Set方法名 - * @return 如果是set或get方法名,返回field, 否则null - */ - public static String getGeneralField(CharSequence getOrSetMethodName) { - final String getOrSetMethodNameStr = getOrSetMethodName.toString(); - if (getOrSetMethodNameStr.startsWith("get") || getOrSetMethodNameStr.startsWith("set")) { - return removePreAndLowerFirst(getOrSetMethodName, 3); - } else if (getOrSetMethodNameStr.startsWith("is")) { - return removePreAndLowerFirst(getOrSetMethodName, 2); - } - return null; - } - - /** - * 生成set方法名
      - * 例如:name 返回 setName - * - * @param fieldName 属性名 - * @return setXxx - */ - public static String genSetter(CharSequence fieldName) { - return upperFirstAndAddPre(fieldName, "set"); - } - - /** - * 生成get方法名 - * - * @param fieldName 属性名 - * @return getXxx - */ - public static String genGetter(CharSequence fieldName) { - return upperFirstAndAddPre(fieldName, "get"); - } - - /** - * 移除字符串中所有给定字符串
      - * 例:removeAll("aa-bb-cc-dd", "-") =》 aabbccdd - * - * @param str 字符串 - * @param strToRemove 被移除的字符串 - * @return 移除后的字符串 - */ - public static String removeAll(CharSequence str, CharSequence strToRemove) { - // strToRemove如果为空, 也不用继续后面的逻辑 - if (isEmpty(str) || isEmpty(strToRemove)) { - return str(str); - } - return str.toString().replace(strToRemove, EMPTY); - } - - /** - * 移除字符串中所有给定字符串,当某个字符串出现多次,则全部移除
      - * 例:removeAny("aa-bb-cc-dd", "a", "b") =》 --cc-dd - * - * @param str 字符串 - * @param strsToRemove 被移除的字符串 - * @return 移除后的字符串 - * @since 5.3.8 - */ - public static String removeAny(CharSequence str, CharSequence... strsToRemove) { - String result = str(str); - if (isNotEmpty(str)) { - for (CharSequence strToRemove : strsToRemove) { - result = removeAll(result, strToRemove); - } - } - return result; - } - - /** - * 去除字符串中指定的多个字符,如有多个则全部去除 - * - * @param str 字符串 - * @param chars 字符列表 - * @return 去除后的字符 - * @since 4.2.2 - */ - public static String removeAll(CharSequence str, char... chars) { - if (null == str || ArrayUtil.isEmpty(chars)) { - return str(str); - } - final int len = str.length(); - if (0 == len) { - return str(str); - } - final StringBuilder builder = builder(len); - char c; - for (int i = 0; i < len; i++) { - c = str.charAt(i); - if (false == ArrayUtil.contains(chars, c)) { - builder.append(c); - } - } - return builder.toString(); - } - - /** - * 去除所有换行符,包括: - * - *
      -	 * 1. \r
      -	 * 1. \n
      -	 * 
      - * - * @param str 字符串 - * @return 处理后的字符串 - * @since 4.2.2 - */ - public static String removeAllLineBreaks(CharSequence str) { - return removeAll(str, C_CR, C_LF); - } - - /** - * 去掉首部指定长度的字符串并将剩余字符串首字母小写
      - * 例如:str=setName, preLength=3 =》 return name - * - * @param str 被处理的字符串 - * @param preLength 去掉的长度 - * @return 处理后的字符串,不符合规范返回null - */ - public static String removePreAndLowerFirst(CharSequence str, int preLength) { - if (str == null) { - return null; - } - if (str.length() > preLength) { - char first = Character.toLowerCase(str.charAt(preLength)); - if (str.length() > preLength + 1) { - return first + str.toString().substring(preLength + 1); - } - return String.valueOf(first); - } else { - return str.toString(); - } - } - - /** - * 去掉首部指定长度的字符串并将剩余字符串首字母小写
      - * 例如:str=setName, prefix=set =》 return name - * - * @param str 被处理的字符串 - * @param prefix 前缀 - * @return 处理后的字符串,不符合规范返回null - */ - public static String removePreAndLowerFirst(CharSequence str, CharSequence prefix) { - return lowerFirst(removePrefix(str, prefix)); - } - - /** - * 原字符串首字母大写并在其首部添加指定字符串 例如:str=name, preString=get =》 return getName - * - * @param str 被处理的字符串 - * @param preString 添加的首部 - * @return 处理后的字符串 - */ - public static String upperFirstAndAddPre(CharSequence str, String preString) { - if (str == null || preString == null) { - return null; - } - return preString + upperFirst(str); - } - - /** - * 大写首字母
      - * 例如:str = name, return Name - * - * @param str 字符串 - * @return 字符串 - */ - public static String upperFirst(CharSequence str) { - if (null == str) { - return null; - } - if (str.length() > 0) { - char firstChar = str.charAt(0); - if (Character.isLowerCase(firstChar)) { - return Character.toUpperCase(firstChar) + subSuf(str, 1); - } - } - return str.toString(); - } - - /** - * 小写首字母
      - * 例如:str = Name, return name - * - * @param str 字符串 - * @return 字符串 - */ - public static String lowerFirst(CharSequence str) { - if (null == str) { - return null; - } - if (str.length() > 0) { - char firstChar = str.charAt(0); - if (Character.isUpperCase(firstChar)) { - return Character.toLowerCase(firstChar) + subSuf(str, 1); - } - } - return str.toString(); - } - - /** - * 去掉指定前缀 - * - * @param str 字符串 - * @param prefix 前缀 - * @return 切掉后的字符串,若前缀不是 preffix, 返回原字符串 - */ - public static String removePrefix(CharSequence str, CharSequence prefix) { - if (isEmpty(str) || isEmpty(prefix)) { - return str(str); - } - - final String str2 = str.toString(); - if (str2.startsWith(prefix.toString())) { - return subSuf(str2, prefix.length());// 截取后半段 - } - return str2; - } - - /** - * 忽略大小写去掉指定前缀 - * - * @param str 字符串 - * @param prefix 前缀 - * @return 切掉后的字符串,若前缀不是 prefix, 返回原字符串 - */ - public static String removePrefixIgnoreCase(CharSequence str, CharSequence prefix) { - if (isEmpty(str) || isEmpty(prefix)) { - return str(str); - } - - final String str2 = str.toString(); - if (str2.toLowerCase().startsWith(prefix.toString().toLowerCase())) { - return subSuf(str2, prefix.length());// 截取后半段 - } - return str2; - } - - /** - * 去掉指定后缀 - * - * @param str 字符串 - * @param suffix 后缀 - * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 - */ - public static String removeSuffix(CharSequence str, CharSequence suffix) { - if (isEmpty(str) || isEmpty(suffix)) { - return str(str); - } - - final String str2 = str.toString(); - if (str2.endsWith(suffix.toString())) { - return subPre(str2, str2.length() - suffix.length());// 截取前半段 - } - return str2; - } - - /** - * 去掉指定后缀,并小写首字母 - * - * @param str 字符串 - * @param suffix 后缀 - * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 - */ - public static String removeSufAndLowerFirst(CharSequence str, CharSequence suffix) { - return lowerFirst(removeSuffix(str, suffix)); - } - - /** - * 忽略大小写去掉指定后缀 - * - * @param str 字符串 - * @param suffix 后缀 - * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 - */ - public static String removeSuffixIgnoreCase(CharSequence str, CharSequence suffix) { - if (isEmpty(str) || isEmpty(suffix)) { - return str(str); - } - - final String str2 = str.toString(); - if (str2.toLowerCase().endsWith(suffix.toString().toLowerCase())) { - return subPre(str2, str2.length() - suffix.length()); - } - return str2; - } - - /** - * 去除两边的指定字符串 - * - * @param str 被处理的字符串 - * @param prefixOrSuffix 前缀或后缀 - * @return 处理后的字符串 - * @since 3.1.2 - */ - public static String strip(CharSequence str, CharSequence prefixOrSuffix) { - if (equals(str, prefixOrSuffix)) { - // 对于去除相同字符的情况单独处理 - return EMPTY; - } - return strip(str, prefixOrSuffix, prefixOrSuffix); - } - - /** - * 去除两边的指定字符串 - * - * @param str 被处理的字符串 - * @param prefix 前缀 - * @param suffix 后缀 - * @return 处理后的字符串 - * @since 3.1.2 - */ - public static String strip(CharSequence str, CharSequence prefix, CharSequence suffix) { - if (isEmpty(str)) { - return str(str); - } - - int from = 0; - int to = str.length(); - - String str2 = str.toString(); - if (startWith(str2, prefix)) { - from = prefix.length(); - } - if (endWith(str2, suffix)) { - to -= suffix.length(); - } - - return str2.substring(Math.min(from, to), Math.max(from, to)); - } - - /** - * 去除两边的指定字符串,忽略大小写 - * - * @param str 被处理的字符串 - * @param prefixOrSuffix 前缀或后缀 - * @return 处理后的字符串 - * @since 3.1.2 - */ - public static String stripIgnoreCase(CharSequence str, CharSequence prefixOrSuffix) { - return stripIgnoreCase(str, prefixOrSuffix, prefixOrSuffix); - } - - /** - * 去除两边的指定字符串,忽略大小写 - * - * @param str 被处理的字符串 - * @param prefix 前缀 - * @param suffix 后缀 - * @return 处理后的字符串 - * @since 3.1.2 - */ - public static String stripIgnoreCase(CharSequence str, CharSequence prefix, CharSequence suffix) { - if (isEmpty(str)) { - return str(str); - } - int from = 0; - int to = str.length(); - - String str2 = str.toString(); - if (startWithIgnoreCase(str2, prefix)) { - from = prefix.length(); - } - if (endWithIgnoreCase(str2, suffix)) { - to -= suffix.length(); - } - return str2.substring(from, to); - } - - /** - * 如果给定字符串不是以prefix开头的,在开头补充 prefix - * - * @param str 字符串 - * @param prefix 前缀 - * @return 补充后的字符串 - */ - public static String addPrefixIfNot(CharSequence str, CharSequence prefix) { - if (isEmpty(str) || isEmpty(prefix)) { - return str(str); - } - - final String str2 = str.toString(); - final String prefix2 = prefix.toString(); - if (false == str2.startsWith(prefix2)) { - return prefix2.concat(str2); - } - return str2; - } - - /** - * 如果给定字符串不是以suffix结尾的,在尾部补充 suffix - * - * @param str 字符串 - * @param suffix 后缀 - * @return 补充后的字符串 - */ - public static String addSuffixIfNot(CharSequence str, CharSequence suffix) { - if (isEmpty(str) || isEmpty(suffix)) { - return str(str); - } - - final String str2 = str.toString(); - final String suffix2 = suffix.toString(); - if (false == str2.endsWith(suffix2)) { - return str2.concat(suffix2); - } - return str2; - } - - /** - * 清理空白字符 - * - * @param str 被清理的字符串 - * @return 清理后的字符串 - */ - public static String cleanBlank(CharSequence str) { - return filter(str, c -> false == CharUtil.isBlankChar(c)); - } - - // ------------------------------------------------------------------------------ Split - - /** - * 切分字符串 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @return 切分后的数组 - */ - public static String[] splitToArray(CharSequence str, char separator) { - return splitToArray(str, separator, 0); - } - - /** - * 切分字符串为long数组 - * - * @param str 被切分的字符串 - * @param separator 分隔符 - * @return 切分后long数组 - * @since 4.0.6 - */ - public static long[] splitToLong(CharSequence str, char separator) { - return Convert.convert(long[].class, splitTrim(str, separator)); - } - - /** - * 切分字符串为long数组 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符串 - * @return 切分后long数组 - * @since 4.0.6 - */ - public static long[] splitToLong(CharSequence str, CharSequence separator) { - return Convert.convert(long[].class, splitTrim(str, separator)); - } - - /** - * 切分字符串为int数组 - * - * @param str 被切分的字符串 - * @param separator 分隔符 - * @return 切分后long数组 - * @since 4.0.6 - */ - public static int[] splitToInt(CharSequence str, char separator) { - return Convert.convert(int[].class, splitTrim(str, separator)); - } - - /** - * 切分字符串为int数组 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符串 - * @return 切分后long数组 - * @since 4.0.6 - */ - public static int[] splitToInt(CharSequence str, CharSequence separator) { - return Convert.convert(int[].class, splitTrim(str, separator)); - } - - /** - * 切分字符串
      - * a#b#c =》 [a,b,c]
      - * a##b#c =》 [a,"",b,c] - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @return 切分后的集合 - */ - public static List split(CharSequence str, char separator) { - return split(str, separator, 0); - } - - /** - * 切分字符串 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @param limit 限制分片数 - * @return 切分后的数组 - */ - public static String[] splitToArray(CharSequence str, char separator, int limit) { - if (null == str) { - return new String[]{}; - } - return StrSpliter.splitToArray(str.toString(), separator, limit, false, false); - } - - /** - * 切分字符串,不去除切分后每个元素两边的空白符,不去除空白项 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @param limit 限制分片数,-1不限制 - * @return 切分后的集合 - */ - public static List split(CharSequence str, char separator, int limit) { - return split(str, separator, limit, false, false); - } - - /** - * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @return 切分后的集合 - * @since 3.1.2 - */ - public static List splitTrim(CharSequence str, char separator) { - return splitTrim(str, separator, -1); - } - - /** - * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @return 切分后的集合 - * @since 3.2.0 - */ - public static List splitTrim(CharSequence str, CharSequence separator) { - return splitTrim(str, separator, -1); - } - - /** - * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @param limit 限制分片数,-1不限制 - * @return 切分后的集合 - * @since 3.1.0 - */ - public static List splitTrim(CharSequence str, char separator, int limit) { - return split(str, separator, limit, true, true); - } - - /** - * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @param limit 限制分片数,-1不限制 - * @return 切分后的集合 - * @since 3.2.0 - */ - public static List splitTrim(CharSequence str, CharSequence separator, int limit) { - return split(str, separator, limit, true, true); - } - - /** - * 切分字符串,不限制分片数量 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @param isTrim 是否去除切分字符串后每个元素两边的空格 - * @param ignoreEmpty 是否忽略空串 - * @return 切分后的集合 - * @since 3.0.8 - */ - public static List split(CharSequence str, char separator, boolean isTrim, boolean ignoreEmpty) { - return split(str, separator, 0, isTrim, ignoreEmpty); - } - - /** - * 切分字符串 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @param limit 限制分片数,-1不限制 - * @param isTrim 是否去除切分字符串后每个元素两边的空格 - * @param ignoreEmpty 是否忽略空串 - * @return 切分后的集合 - * @since 3.0.8 - */ - public static List split(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { - if (null == str) { - return new ArrayList<>(0); - } - return StrSpliter.split(str.toString(), separator, limit, isTrim, ignoreEmpty); - } - - /** - * 切分字符串 - * - * @param str 被切分的字符串 - * @param separator 分隔符字符 - * @param limit 限制分片数,-1不限制 - * @param isTrim 是否去除切分字符串后每个元素两边的空格 - * @param ignoreEmpty 是否忽略空串 - * @return 切分后的集合 - * @since 3.2.0 - */ - public static List split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) { - if (null == str) { - return new ArrayList<>(0); - } - final String separatorStr = (null == separator) ? null : separator.toString(); - return StrSpliter.split(str.toString(), separatorStr, limit, isTrim, ignoreEmpty); - } - - /** - * 切分字符串,如果分隔符不存在则返回原字符串 - * - * @param str 被切分的字符串 - * @param separator 分隔符 - * @return 字符串 - */ - public static String[] split(CharSequence str, CharSequence separator) { - if (str == null) { - return new String[]{}; - } - - final String separatorStr = (null == separator) ? null : separator.toString(); - return StrSpliter.splitToArray(str.toString(), separatorStr, 0, false, false); - } - - /** - * 根据给定长度,将给定字符串截取为多个部分 - * - * @param str 字符串 - * @param len 每一个小节的长度 - * @return 截取后的字符串数组 - * @see StrSpliter#splitByLength(String, int) - */ - public static String[] split(CharSequence str, int len) { - if (null == str) { - return new String[]{}; - } - return StrSpliter.splitByLength(str.toString(), len); - } - - /** - * 改进JDK subString
      - * index从0开始计算,最后一个字符为-1
      - * 如果from和to位置一样,返回 ""
      - * 如果from或to为负数,则按照length从后向前数位置,如果绝对值大于字符串长度,则from归到0,to归到length
      - * 如果经过修正的index中from大于to,则互换from和to example:
      - * abcdefgh 2 3 =》 c
      - * abcdefgh 2 -3 =》 cde
      - * - * @param str String - * @param fromIndexInclude 开始的index(包括) - * @param toIndexExclude 结束的index(不包括) - * @return 字串 - */ - public static String sub(CharSequence str, int fromIndexInclude, int toIndexExclude) { - if (isEmpty(str)) { - return str(str); - } - int len = str.length(); - - if (fromIndexInclude < 0) { - fromIndexInclude = len + fromIndexInclude; - if (fromIndexInclude < 0) { - fromIndexInclude = 0; - } - } else if (fromIndexInclude > len) { - fromIndexInclude = len; - } - - if (toIndexExclude < 0) { - toIndexExclude = len + toIndexExclude; - if (toIndexExclude < 0) { - toIndexExclude = len; - } - } else if (toIndexExclude > len) { - toIndexExclude = len; - } - - if (toIndexExclude < fromIndexInclude) { - int tmp = fromIndexInclude; - fromIndexInclude = toIndexExclude; - toIndexExclude = tmp; - } - - if (fromIndexInclude == toIndexExclude) { - return EMPTY; - } - - return str.toString().substring(fromIndexInclude, toIndexExclude); - } - - /** - * 通过CodePoint截取字符串,可以截断Emoji - * - * @param str String - * @param fromIndex 开始的index(包括) - * @param toIndex 结束的index(不包括) - * @return 字串 - */ - public static String subByCodePoint(CharSequence str, int fromIndex, int toIndex) { - if (isEmpty(str)) { - return str(str); - } - - if (fromIndex < 0 || fromIndex > toIndex) { - throw new IllegalArgumentException(); - } - - if (fromIndex == toIndex) { - return EMPTY; - } - - final StringBuilder sb = new StringBuilder(); - final int subLen = toIndex - fromIndex; - str.toString().codePoints().skip(fromIndex).limit(subLen).forEach(v -> sb.append(Character.toChars(v))); - return sb.toString(); - } - - /** - * 截取部分字符串,这里一个汉字的长度认为是2 - * - * @param str 字符串 - * @param len 切割的位置 - * @param suffix 切割后加上后缀 - * @return 切割后的字符串 - * @since 3.1.1 - */ - public static String subPreGbk(CharSequence str, int len, CharSequence suffix) { - if (isEmpty(str)) { - return str(str); - } - - byte[] b; - int counterOfDoubleByte = 0; - b = str.toString().getBytes(CharsetUtil.CHARSET_GBK); - if (b.length <= len) { - return str.toString(); - } - for (int i = 0; i < len; i++) { - if (b[i] < 0) { - counterOfDoubleByte++; - } - } - - if (counterOfDoubleByte % 2 != 0) { - len += 1; - } - return new String(b, 0, len, CharsetUtil.CHARSET_GBK) + suffix; - } - - /** - * 限制字符串长度,如果超过指定长度,截取指定长度并在末尾加"..." - * - * @param string 字符串 - * @param length 最大长度 - * @return 切割后的剩余的前半部分字符串+"..." - * @since 4.0.10 - */ - public static String maxLength(CharSequence string, int length) { - Assert.isTrue(length > 0); - if (null == string) { - return null; - } - if (string.length() <= length) { - return string.toString(); - } - return sub(string, 0, length) + "..."; - } - - /** - * 切割指定位置之前部分的字符串 - * - * @param string 字符串 - * @param toIndexExclude 切割到的位置(不包括) - * @return 切割后的剩余的前半部分字符串 - */ - public static String subPre(CharSequence string, int toIndexExclude) { - return sub(string, 0, toIndexExclude); - } - - /** - * 切割指定位置之后部分的字符串 - * - * @param string 字符串 - * @param fromIndex 切割开始的位置(包括) - * @return 切割后后剩余的后半部分字符串 - */ - public static String subSuf(CharSequence string, int fromIndex) { - if (isEmpty(string)) { - return null; - } - return sub(string, fromIndex, string.length()); - } - - /** - * 切割指定长度的后部分的字符串 - * - *
      -	 * StrUtil.subSufByLength("abcde", 3)      =    "cde"
      -	 * StrUtil.subSufByLength("abcde", 0)      =    ""
      -	 * StrUtil.subSufByLength("abcde", -5)     =    ""
      -	 * StrUtil.subSufByLength("abcde", -1)     =    ""
      -	 * StrUtil.subSufByLength("abcde", 5)       =    "abcde"
      -	 * StrUtil.subSufByLength("abcde", 10)     =    "abcde"
      -	 * StrUtil.subSufByLength(null, 3)               =    null
      -	 * 
      - * - * @param string 字符串 - * @param length 切割长度 - * @return 切割后后剩余的后半部分字符串 - * @since 4.0.1 - */ - public static String subSufByLength(CharSequence string, int length) { - if (isEmpty(string)) { - return null; - } - if (length <= 0) { - return EMPTY; - } - return sub(string, -length, string.length()); - } - - /** - * 截取字符串,从指定位置开始,截取指定长度的字符串
      - * author weibaohui - * - * @param input 原始字符串 - * @param fromIndex 开始的index,包括 - * @param length 要截取的长度 - * @return 截取后的字符串 - */ - public static String subWithLength(String input, int fromIndex, int length) { - return sub(input, fromIndex, fromIndex + length); - } - - /** - * 截取分隔字符串之前的字符串,不包括分隔字符串
      - * 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
      - * 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串,举例如下: - * - *
      -	 * StrUtil.subBefore(null, *, false)      = null
      -	 * StrUtil.subBefore("", *, false)        = ""
      -	 * StrUtil.subBefore("abc", "a", false)   = ""
      -	 * StrUtil.subBefore("abcba", "b", false) = "a"
      -	 * StrUtil.subBefore("abc", "c", false)   = "ab"
      -	 * StrUtil.subBefore("abc", "d", false)   = "abc"
      -	 * StrUtil.subBefore("abc", "", false)    = ""
      -	 * StrUtil.subBefore("abc", null, false)  = "abc"
      -	 * 
      - * - * @param string 被查找的字符串 - * @param separator 分隔字符串(不包括) - * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 - * @return 切割后的字符串 - * @since 3.1.1 - */ - public static String subBefore(CharSequence string, CharSequence separator, boolean isLastSeparator) { - if (isEmpty(string) || separator == null) { - return null == string ? null : string.toString(); - } - - final String str = string.toString(); - final String sep = separator.toString(); - if (sep.isEmpty()) { - return EMPTY; - } - final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); - if (INDEX_NOT_FOUND == pos) { - return str; - } - if (0 == pos) { - return EMPTY; - } - return str.substring(0, pos); - } - - /** - * 截取分隔字符串之前的字符串,不包括分隔字符串
      - * 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
      - * 如果分隔字符串未找到,返回原字符串,举例如下: - * - *
      -	 * StrUtil.subBefore(null, *, false)      = null
      -	 * StrUtil.subBefore("", *, false)        = ""
      -	 * StrUtil.subBefore("abc", 'a', false)   = ""
      -	 * StrUtil.subBefore("abcba", 'b', false) = "a"
      -	 * StrUtil.subBefore("abc", 'c', false)   = "ab"
      -	 * StrUtil.subBefore("abc", 'd', false)   = "abc"
      -	 * 
      - * - * @param string 被查找的字符串 - * @param separator 分隔字符串(不包括) - * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 - * @return 切割后的字符串 - * @since 4.1.15 - */ - public static String subBefore(CharSequence string, char separator, boolean isLastSeparator) { - if (isEmpty(string)) { - return null == string ? null : EMPTY; - } - - final String str = string.toString(); - final int pos = isLastSeparator ? str.lastIndexOf(separator) : str.indexOf(separator); - if (INDEX_NOT_FOUND == pos) { - return str; - } - if (0 == pos) { - return EMPTY; - } - return str.substring(0, pos); - } - - /** - * 截取分隔字符串之后的字符串,不包括分隔字符串
      - * 如果给定的字符串为空串(null或""),返回原字符串
      - * 如果分隔字符串为空串(null或""),则返回空串,如果分隔字符串未找到,返回空串,举例如下: - * - *
      -	 * StrUtil.subAfter(null, *, false)      = null
      -	 * StrUtil.subAfter("", *, false)        = ""
      -	 * StrUtil.subAfter(*, null, false)      = ""
      -	 * StrUtil.subAfter("abc", "a", false)   = "bc"
      -	 * StrUtil.subAfter("abcba", "b", false) = "cba"
      -	 * StrUtil.subAfter("abc", "c", false)   = ""
      -	 * StrUtil.subAfter("abc", "d", false)   = ""
      -	 * StrUtil.subAfter("abc", "", false)    = "abc"
      -	 * 
      - * - * @param string 被查找的字符串 - * @param separator 分隔字符串(不包括) - * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 - * @return 切割后的字符串 - * @since 3.1.1 - */ - public static String subAfter(CharSequence string, CharSequence separator, boolean isLastSeparator) { - if (isEmpty(string)) { - return null == string ? null : EMPTY; - } - if (separator == null) { - return EMPTY; - } - final String str = string.toString(); - final String sep = separator.toString(); - final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); - if (INDEX_NOT_FOUND == pos || (string.length() - 1) == pos) { - return EMPTY; - } - return str.substring(pos + separator.length()); - } - - /** - * 截取分隔字符串之后的字符串,不包括分隔字符串
      - * 如果给定的字符串为空串(null或""),返回原字符串
      - * 如果分隔字符串为空串(null或""),则返回空串,如果分隔字符串未找到,返回空串,举例如下: - * - *
      -	 * StrUtil.subAfter(null, *, false)      = null
      -	 * StrUtil.subAfter("", *, false)        = ""
      -	 * StrUtil.subAfter("abc", 'a', false)   = "bc"
      -	 * StrUtil.subAfter("abcba", 'b', false) = "cba"
      -	 * StrUtil.subAfter("abc", 'c', false)   = ""
      -	 * StrUtil.subAfter("abc", 'd', false)   = ""
      -	 * 
      - * - * @param string 被查找的字符串 - * @param separator 分隔字符串(不包括) - * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 - * @return 切割后的字符串 - * @since 4.1.15 - */ - public static String subAfter(CharSequence string, char separator, boolean isLastSeparator) { - if (isEmpty(string)) { - return null == string ? null : EMPTY; - } - final String str = string.toString(); - final int pos = isLastSeparator ? str.lastIndexOf(separator) : str.indexOf(separator); - if (INDEX_NOT_FOUND == pos) { - return EMPTY; - } - return str.substring(pos + 1); - } - - /** - * 截取指定字符串中间部分,不包括标识字符串
      - *

      - * 栗子: - * - *

      -	 * StrUtil.subBetween("wx[b]yz", "[", "]") = "b"
      -	 * StrUtil.subBetween(null, *, *)          = null
      -	 * StrUtil.subBetween(*, null, *)          = null
      -	 * StrUtil.subBetween(*, *, null)          = null
      -	 * StrUtil.subBetween("", "", "")          = ""
      -	 * StrUtil.subBetween("", "", "]")         = null
      -	 * StrUtil.subBetween("", "[", "]")        = null
      -	 * StrUtil.subBetween("yabcz", "", "")     = ""
      -	 * StrUtil.subBetween("yabcz", "y", "z")   = "abc"
      -	 * StrUtil.subBetween("yabczyabcz", "y", "z")   = "abc"
      -	 * 
      - * - * @param str 被切割的字符串 - * @param before 截取开始的字符串标识 - * @param after 截取到的字符串标识 - * @return 截取后的字符串 - * @since 3.1.1 - */ - public static String subBetween(CharSequence str, CharSequence before, CharSequence after) { - if (str == null || before == null || after == null) { - return null; - } - - final String str2 = str.toString(); - final String before2 = before.toString(); - final String after2 = after.toString(); - - final int start = str2.indexOf(before2); - if (start != INDEX_NOT_FOUND) { - final int end = str2.indexOf(after2, start + before2.length()); - if (end != INDEX_NOT_FOUND) { - return str2.substring(start + before2.length(), end); - } - } - return null; - } - - /** - * 截取指定字符串中间部分,不包括标识字符串
      - *

      - * 栗子: - * - *

      -	 * StrUtil.subBetween(null, *)            = null
      -	 * StrUtil.subBetween("", "")             = ""
      -	 * StrUtil.subBetween("", "tag")          = null
      -	 * StrUtil.subBetween("tagabctag", null)  = null
      -	 * StrUtil.subBetween("tagabctag", "")    = ""
      -	 * StrUtil.subBetween("tagabctag", "tag") = "abc"
      -	 * 
      - * - * @param str 被切割的字符串 - * @param beforeAndAfter 截取开始和结束的字符串标识 - * @return 截取后的字符串 - * @since 3.1.1 - */ - public static String subBetween(CharSequence str, CharSequence beforeAndAfter) { - return subBetween(str, beforeAndAfter, beforeAndAfter); - } - - /** - * 截取指定字符串多段中间部分,不包括标识字符串
      - *

      - * 栗子: - * - *

      -	 * StrUtil.subBetweenAll("wx[b]y[z]", "[", "]") 		= ["b","z"]
      -	 * StrUtil.subBetweenAll(null, *, *)          			= []
      -	 * StrUtil.subBetweenAll(*, null, *)          			= []
      -	 * StrUtil.subBetweenAll(*, *, null)          			= []
      -	 * StrUtil.subBetweenAll("", "", "")          			= []
      -	 * StrUtil.subBetweenAll("", "", "]")         			= []
      -	 * StrUtil.subBetweenAll("", "[", "]")        			= []
      -	 * StrUtil.subBetweenAll("yabcz", "", "")     			= []
      -	 * StrUtil.subBetweenAll("yabcz", "y", "z")   			= ["abc"]
      -	 * StrUtil.subBetweenAll("yabczyabcz", "y", "z")   		= ["abc","abc"]
      -	 * StrUtil.subBetweenAll("[yabc[zy]abcz]", "[", "]");   = ["zy"]           重叠时只截取内部,
      -	 * 
      - * - * @param str 被切割的字符串 - * @param prefix 截取开始的字符串标识 - * @param suffix 截取到的字符串标识 - * @return 截取后的字符串 - * @author dahuoyzs - * @since 5.2.5 - */ - public static String[] subBetweenAll(CharSequence str, CharSequence prefix, CharSequence suffix) { - if (hasEmpty(str, prefix, suffix) || - // 不包含起始字符串,则肯定没有子串 - false == contains(str, prefix)) { - return new String[0]; - } - - final List result = new LinkedList<>(); - final String[] split = split(str, prefix); - if (prefix.equals(suffix)) { - // 前后缀字符相同,单独处理 - for (int i = 1, length = split.length - 1; i < length; i += 2) { - result.add(split[i]); - } - } else { - int suffixIndex; - for (String fragment : split) { - suffixIndex = fragment.indexOf(suffix.toString()); - if (suffixIndex > 0) { - result.add(fragment.substring(0, suffixIndex)); - } - } - } - - return result.toArray(new String[0]); - } - - /** - * 截取指定字符串多段中间部分,不包括标识字符串
      - *

      - * 栗子: - * - *

      -	 * StrUtil.subBetweenAll(null, *)          			= []
      -	 * StrUtil.subBetweenAll(*, null)          			= []
      -	 * StrUtil.subBetweenAll(*, *)          			= []
      -	 * StrUtil.subBetweenAll("", "")          			= []
      -	 * StrUtil.subBetweenAll("", "#")         			= []
      -	 * StrUtil.subBetweenAll("gotanks", "")     		= []
      -	 * StrUtil.subBetweenAll("#gotanks#", "#")   		= ["gotanks"]
      -	 * StrUtil.subBetweenAll("#hello# #world#!", "#")   = ["hello", "world"]
      -	 * StrUtil.subBetweenAll("#hello# world#!", "#");   = ["hello"]
      -	 * 
      - * - * @param str 被切割的字符串 - * @param prefixAndSuffix 截取开始和结束的字符串标识 - * @return 截取后的字符串 - * @author gotanks - * @since 5.5.0 - */ - public static String[] subBetweenAll(CharSequence str, CharSequence prefixAndSuffix) { - return subBetweenAll(str, prefixAndSuffix, prefixAndSuffix); - } - - /** - * 给定字符串是否被字符包围 - * - * @param str 字符串 - * @param prefix 前缀 - * @param suffix 后缀 - * @return 是否包围,空串不包围 - */ - public static boolean isSurround(CharSequence str, CharSequence prefix, CharSequence suffix) { - if (StrUtil.isBlank(str)) { - return false; - } - if (str.length() < (prefix.length() + suffix.length())) { - return false; - } - - final String str2 = str.toString(); - return str2.startsWith(prefix.toString()) && str2.endsWith(suffix.toString()); - } - - /** - * 给定字符串是否被字符包围 - * - * @param str 字符串 - * @param prefix 前缀 - * @param suffix 后缀 - * @return 是否包围,空串不包围 - */ - public static boolean isSurround(CharSequence str, char prefix, char suffix) { - if (StrUtil.isBlank(str)) { - return false; - } - if (str.length() < 2) { - return false; - } - - return str.charAt(0) == prefix && str.charAt(str.length() - 1) == suffix; - } - - /** - * 重复某个字符 - * - * @param c 被重复的字符 - * @param count 重复的数目,如果小于等于0则返回"" - * @return 重复字符字符串 - */ - public static String repeat(char c, int count) { - if (count <= 0) { - return EMPTY; - } - - char[] result = new char[count]; - for (int i = 0; i < count; i++) { - result[i] = c; - } - return new String(result); - } - - /** - * 重复某个字符串 - * - * @param str 被重复的字符 - * @param count 重复的数目 - * @return 重复字符字符串 - */ - public static String repeat(CharSequence str, int count) { - if (null == str) { - return null; - } - if (count <= 0 || str.length() == 0) { - return EMPTY; - } - if (count == 1) { - return str.toString(); - } - - // 检查 - final int len = str.length(); - final long longSize = (long) len * (long) count; - final int size = (int) longSize; - if (size != longSize) { - throw new ArrayIndexOutOfBoundsException("Required String length is too large: " + longSize); - } - - final char[] array = new char[size]; - str.toString().getChars(0, len, array, 0); - int n; - for (n = len; n < size - n; n <<= 1) {// n <<= 1相当于n *2 - System.arraycopy(array, 0, array, n, n); - } - System.arraycopy(array, 0, array, n, size - n); - return new String(array); - } - - /** - * 重复某个字符串到指定长度 - * - * @param str 被重复的字符 - * @param padLen 指定长度 - * @return 重复字符字符串 - * @since 4.3.2 - */ - public static String repeatByLength(CharSequence str, int padLen) { - if (null == str) { - return null; - } - if (padLen <= 0) { - return StrUtil.EMPTY; - } - final int strLen = str.length(); - if (strLen == padLen) { - return str.toString(); - } else if (strLen > padLen) { - return subPre(str, padLen); - } - - // 重复,直到达到指定长度 - final char[] padding = new char[padLen]; - for (int i = 0; i < padLen; i++) { - padding[i] = str.charAt(i % strLen); - } - return new String(padding); - } - - /** - * 重复某个字符串并通过分界符连接 - * - *
      -	 * StrUtil.repeatAndJoin("?", 5, ",")   = "?,?,?,?,?"
      -	 * StrUtil.repeatAndJoin("?", 0, ",")   = ""
      -	 * StrUtil.repeatAndJoin("?", 5, null) = "?????"
      -	 * 
      - * - * @param str 被重复的字符串 - * @param count 数量 - * @param conjunction 分界符 - * @return 连接后的字符串 - * @since 4.0.1 - */ - public static String repeatAndJoin(CharSequence str, int count, CharSequence conjunction) { - if (count <= 0) { - return EMPTY; - } - final StrBuilder builder = StrBuilder.create(); - boolean isFirst = true; - while (count-- > 0) { - if (isFirst) { - isFirst = false; - } else if (isNotEmpty(conjunction)) { - builder.append(conjunction); - } - builder.append(str); - } - return builder.toString(); - } - - /** - * 比较两个字符串(大小写敏感)。 - * - *
      -	 * equals(null, null)   = true
      -	 * equals(null, "abc")  = false
      -	 * equals("abc", null)  = false
      -	 * equals("abc", "abc") = true
      -	 * equals("abc", "ABC") = false
      -	 * 
      - * - * @param str1 要比较的字符串1 - * @param str2 要比较的字符串2 - * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} - */ - public static boolean equals(CharSequence str1, CharSequence str2) { - return equals(str1, str2, false); - } - - /** - * 比较两个字符串(大小写不敏感)。 - * - *
      -	 * equalsIgnoreCase(null, null)   = true
      -	 * equalsIgnoreCase(null, "abc")  = false
      -	 * equalsIgnoreCase("abc", null)  = false
      -	 * equalsIgnoreCase("abc", "abc") = true
      -	 * equalsIgnoreCase("abc", "ABC") = true
      -	 * 
      - * - * @param str1 要比较的字符串1 - * @param str2 要比较的字符串2 - * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} - */ - public static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) { - return equals(str1, str2, true); - } - - /** - * 比较两个字符串是否相等。 - * - * @param str1 要比较的字符串1 - * @param str2 要比较的字符串2 - * @param ignoreCase 是否忽略大小写 - * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} - * @since 3.2.0 - */ - public static boolean equals(CharSequence str1, CharSequence str2, boolean ignoreCase) { - if (null == str1) { - // 只有两个都为null才判断相等 - return str2 == null; - } - if (null == str2) { - // 字符串2空,字符串1非空,直接false - return false; - } - - if (ignoreCase) { - return str1.toString().equalsIgnoreCase(str2.toString()); - } else { - return str1.toString().contentEquals(str2); - } - } - - /** - * 给定字符串是否与提供的中任一字符串相同(忽略大小写),相同则返回{@code true},没有相同的返回{@code false}
      - * 如果参与比对的字符串列表为空,返回{@code false} - * - * @param str1 给定需要检查的字符串 - * @param strs 需要参与比对的字符串列表 - * @return 是否相同 - * @since 4.3.2 - */ - public static boolean equalsAnyIgnoreCase(CharSequence str1, CharSequence... strs) { - return equalsAny(str1, true, strs); - } - - /** - * 给定字符串是否与提供的中任一字符串相同,相同则返回{@code true},没有相同的返回{@code false}
      - * 如果参与比对的字符串列表为空,返回{@code false} - * - * @param str1 给定需要检查的字符串 - * @param strs 需要参与比对的字符串列表 - * @return 是否相同 - * @since 4.3.2 - */ - public static boolean equalsAny(CharSequence str1, CharSequence... strs) { - return equalsAny(str1, false, strs); - } - - /** - * 给定字符串是否与提供的中任一字符串相同,相同则返回{@code true},没有相同的返回{@code false}
      - * 如果参与比对的字符串列表为空,返回{@code false} - * - * @param str1 给定需要检查的字符串 - * @param ignoreCase 是否忽略大小写 - * @param strs 需要参与比对的字符串列表 - * @return 是否相同 - * @since 4.3.2 - */ - public static boolean equalsAny(CharSequence str1, boolean ignoreCase, CharSequence... strs) { - if (ArrayUtil.isEmpty(strs)) { - return false; - } - - for (CharSequence str : strs) { - if (equals(str1, str, ignoreCase)) { - return true; - } - } - return false; - } - - /** - * 格式化文本, {} 表示占位符
      - * 此方法只是简单将占位符 {} 按照顺序替换为参数
      - * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
      - * 例:
      - * 通常使用:format("this is {} for {}", "a", "b") =》 this is a for b
      - * 转义{}: format("this is \\{} for {}", "a", "b") =》 this is \{} for a
      - * 转义\: format("this is \\\\{} for {}", "a", "b") =》 this is \a for b
      - * - * @param template 文本模板,被替换的部分用 {} 表示,如果模板为null,返回"null" - * @param params 参数值 - * @return 格式化后的文本,如果模板为null,返回"null" - */ - public static String format(CharSequence template, Object... params) { - if (null == template) { - return NULL; - } - if (ArrayUtil.isEmpty(params) || isBlank(template)) { - return template.toString(); - } - return StrFormatter.format(template.toString(), params); - } - - /** - * 有序的格式化文本,使用{number}做为占位符
      - * 通常使用:format("this is {0} for {1}", "a", "b") =》 this is a for b
      - * - * @param pattern 文本格式 - * @param arguments 参数 - * @return 格式化后的文本 - */ - public static String indexedFormat(CharSequence pattern, Object... arguments) { - return MessageFormat.format(pattern.toString(), arguments); - } - - /** - * 格式化文本,使用 {varName} 占位
      - * map = {a: "aValue", b: "bValue"} format("{a} and {b}", map) ---=》 aValue and bValue - * - * @param template 文本模板,被替换的部分用 {key} 表示 - * @param map 参数值对 - * @return 格式化后的文本 - */ - public static String format(CharSequence template, Map map) { - return format(template, map, true); - } - - /** - * 格式化文本,使用 {varName} 占位
      - * map = {a: "aValue", b: "bValue"} format("{a} and {b}", map) ---=》 aValue and bValue - * - * @param template 文本模板,被替换的部分用 {key} 表示 - * @param map 参数值对 - * @param ignoreNull 是否忽略 {@code null} 值,忽略则 {@code null} 值对应的变量不被替换,否则替换为"" - * @return 格式化后的文本 - * @since 5.4.3 - */ - public static String format(CharSequence template, Map map, boolean ignoreNull) { - if (null == template) { - return null; - } - if (null == map || map.isEmpty()) { - return template.toString(); - } - - String template2 = template.toString(); - String value; - for (Entry entry : map.entrySet()) { - value = utf8Str(entry.getValue()); - if (null == value && ignoreNull) { - continue; - } - template2 = replace(template2, "{" + entry.getKey() + "}", value); - } - return template2; - } - - /** - * 编码字符串,编码为UTF-8 - * - * @param str 字符串 - * @return 编码后的字节码 - */ - public static byte[] utf8Bytes(CharSequence str) { - return bytes(str, CharsetUtil.CHARSET_UTF_8); - } - - /** - * 编码字符串
      - * 使用系统默认编码 - * - * @param str 字符串 - * @return 编码后的字节码 - */ - public static byte[] bytes(CharSequence str) { - return bytes(str, Charset.defaultCharset()); - } - - /** - * 编码字符串 - * - * @param str 字符串 - * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 - * @return 编码后的字节码 - */ - public static byte[] bytes(CharSequence str, String charset) { - return bytes(str, isBlank(charset) ? Charset.defaultCharset() : Charset.forName(charset)); - } - - /** - * 编码字符串 - * - * @param str 字符串 - * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 - * @return 编码后的字节码 - */ - public static byte[] bytes(CharSequence str, Charset charset) { - if (str == null) { - return null; - } - - if (null == charset) { - return str.toString().getBytes(); - } - return str.toString().getBytes(charset); - } - /** * 将对象转为字符串
      * @@ -2998,16 +456,6 @@ public class StrUtil { return charset.decode(data).toString(); } - /** - * {@link CharSequence} 转为字符串,null安全 - * - * @param cs {@link CharSequence} - * @return 字符串 - */ - public static String str(CharSequence cs) { - return null == cs ? null : cs.toString(); - } - /** * 调用对象的toString方法,null会返回“null” * @@ -3019,556 +467,6 @@ public class StrUtil { return null == obj ? NULL : obj.toString(); } - /** - * 字符串转换为byteBuffer - * - * @param str 字符串 - * @param charset 编码 - * @return byteBuffer - */ - public static ByteBuffer byteBuffer(CharSequence str, String charset) { - return ByteBuffer.wrap(bytes(str, charset)); - } - - /** - * 以 conjunction 为分隔符将多个对象转换为字符串 - * - * @param conjunction 分隔符 - * @param objs 数组 - * @return 连接后的字符串 - * @see ArrayUtil#join(Object, CharSequence) - */ - public static String join(CharSequence conjunction, Object... objs) { - return ArrayUtil.join(objs, conjunction); - } - - /** - * 将驼峰式命名的字符串转换为下划线方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。
      - * 例如: - * - *
      -	 * HelloWorld=》hello_world
      -	 * Hello_World=》hello_world
      -	 * HelloWorld_test=》hello_world_test
      -	 * 
      - * - * @param str 转换前的驼峰式命名的字符串,也可以为下划线形式 - * @return 转换后下划线方式命名的字符串 - */ - public static String toUnderlineCase(CharSequence str) { - return toSymbolCase(str, CharUtil.UNDERLINE); - } - - /** - * 将驼峰式命名的字符串转换为使用符号连接方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。
      - * - * @param str 转换前的驼峰式命名的字符串,也可以为符号连接形式 - * @param symbol 连接符 - * @return 转换后符号连接方式命名的字符串 - * @since 4.0.10 - */ - public static String toSymbolCase(CharSequence str, char symbol) { - if (str == null) { - return null; - } - - final int length = str.length(); - final StrBuilder sb = new StrBuilder(); - char c; - for (int i = 0; i < length; i++) { - c = str.charAt(i); - final Character preChar = (i > 0) ? str.charAt(i - 1) : null; - if (Character.isUpperCase(c)) { - // 遇到大写字母处理 - final Character nextChar = (i < str.length() - 1) ? str.charAt(i + 1) : null; - if (null != preChar && Character.isUpperCase(preChar)) { - // 前一个字符为大写,则按照一个词对待,例如AB - sb.append(c); - } else if (null != nextChar && (false == Character.isLowerCase(nextChar))) { - // 后一个为非小写字母,按照一个词对待 - if (null != preChar && symbol != preChar) { - // 前一个是非大写时按照新词对待,加连接符,例如xAB - sb.append(symbol); - } - sb.append(c); - } else { - // 前后都为非大写按照新词对待 - if (null != preChar && symbol != preChar) { - // 前一个非连接符,补充连接符 - sb.append(symbol); - } - sb.append(Character.toLowerCase(c)); - } - } else { - if (symbol != c - && sb.length() > 0 - && Character.isUpperCase(sb.charAt(-1)) - && Character.isLowerCase(c)) { - // 当结果中前一个字母为大写,当前为小写(非数字或字符),说明此字符为新词开始(连接符也表示新词) - sb.append(symbol); - } - // 小写或符号 - sb.append(c); - } - } - return sb.toString(); - } - - /** - * 将下划线方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。
      - * 例如:hello_world=》helloWorld - * - * @param name 转换前的下划线大写方式命名的字符串 - * @return 转换后的驼峰式命名的字符串 - */ - public static String toCamelCase(CharSequence name) { - if (null == name) { - return null; - } - - String name2 = name.toString(); - if (name2.contains(UNDERLINE)) { - final StringBuilder sb = new StringBuilder(name2.length()); - boolean upperCase = false; - for (int i = 0; i < name2.length(); i++) { - char c = name2.charAt(i); - - if (c == CharUtil.UNDERLINE) { - upperCase = true; - } else if (upperCase) { - sb.append(Character.toUpperCase(c)); - upperCase = false; - } else { - sb.append(Character.toLowerCase(c)); - } - } - return sb.toString(); - } else { - return name2; - } - } - - /** - * 包装指定字符串
      - * 当前缀和后缀一致时使用此方法 - * - * @param str 被包装的字符串 - * @param prefixAndSuffix 前缀和后缀 - * @return 包装后的字符串 - * @since 3.1.0 - */ - public static String wrap(CharSequence str, CharSequence prefixAndSuffix) { - return wrap(str, prefixAndSuffix, prefixAndSuffix); - } - - /** - * 包装指定字符串 - * - * @param str 被包装的字符串 - * @param prefix 前缀 - * @param suffix 后缀 - * @return 包装后的字符串 - */ - public static String wrap(CharSequence str, CharSequence prefix, CharSequence suffix) { - return nullToEmpty(prefix).concat(nullToEmpty(str)).concat(nullToEmpty(suffix)); - } - - /** - * 使用单个字符包装多个字符串 - * - * @param prefixAndSuffix 前缀和后缀 - * @param strs 多个字符串 - * @return 包装的字符串数组 - * @since 5.4.1 - */ - public static String[] wrapAllWithPair(CharSequence prefixAndSuffix, CharSequence... strs) { - return wrapAll(prefixAndSuffix, prefixAndSuffix, strs); - } - - /** - * 包装多个字符串 - * - * @param prefix 前缀 - * @param suffix 后缀 - * @param strs 多个字符串 - * @return 包装的字符串数组 - * @since 4.0.7 - */ - public static String[] wrapAll(CharSequence prefix, CharSequence suffix, CharSequence... strs) { - final String[] results = new String[strs.length]; - for (int i = 0; i < strs.length; i++) { - results[i] = wrap(strs[i], prefix, suffix); - } - return results; - } - - /** - * 包装指定字符串,如果前缀或后缀已经包含对应的字符串,则不再包装 - * - * @param str 被包装的字符串 - * @param prefix 前缀 - * @param suffix 后缀 - * @return 包装后的字符串 - */ - public static String wrapIfMissing(CharSequence str, CharSequence prefix, CharSequence suffix) { - int len = 0; - if (isNotEmpty(str)) { - len += str.length(); - } - if (isNotEmpty(prefix)) { - len += str.length(); - } - if (isNotEmpty(suffix)) { - len += str.length(); - } - StringBuilder sb = new StringBuilder(len); - if (isNotEmpty(prefix) && false == startWith(str, prefix)) { - sb.append(prefix); - } - if (isNotEmpty(str)) { - sb.append(str); - } - if (isNotEmpty(suffix) && false == endWith(str, suffix)) { - sb.append(suffix); - } - return sb.toString(); - } - - /** - * 使用成对的字符包装多个字符串,如果已经包装,则不再包装 - * - * @param prefixAndSuffix 前缀和后缀 - * @param strs 多个字符串 - * @return 包装的字符串数组 - * @since 5.4.1 - */ - public static String[] wrapAllWithPairIfMissing(CharSequence prefixAndSuffix, CharSequence... strs) { - return wrapAllIfMissing(prefixAndSuffix, prefixAndSuffix, strs); - } - - /** - * 包装多个字符串,如果已经包装,则不再包装 - * - * @param prefix 前缀 - * @param suffix 后缀 - * @param strs 多个字符串 - * @return 包装的字符串数组 - * @since 4.0.7 - */ - public static String[] wrapAllIfMissing(CharSequence prefix, CharSequence suffix, CharSequence... strs) { - final String[] results = new String[strs.length]; - for (int i = 0; i < strs.length; i++) { - results[i] = wrapIfMissing(strs[i], prefix, suffix); - } - return results; - } - - /** - * 去掉字符包装,如果未被包装则返回原字符串 - * - * @param str 字符串 - * @param prefix 前置字符串 - * @param suffix 后置字符串 - * @return 去掉包装字符的字符串 - * @since 4.0.1 - */ - public static String unWrap(CharSequence str, String prefix, String suffix) { - if (isWrap(str, prefix, suffix)) { - return sub(str, prefix.length(), str.length() - suffix.length()); - } - return str.toString(); - } - - /** - * 去掉字符包装,如果未被包装则返回原字符串 - * - * @param str 字符串 - * @param prefix 前置字符 - * @param suffix 后置字符 - * @return 去掉包装字符的字符串 - * @since 4.0.1 - */ - public static String unWrap(CharSequence str, char prefix, char suffix) { - if (isEmpty(str)) { - return str(str); - } - if (str.charAt(0) == prefix && str.charAt(str.length() - 1) == suffix) { - return sub(str, 1, str.length() - 1); - } - return str.toString(); - } - - /** - * 去掉字符包装,如果未被包装则返回原字符串 - * - * @param str 字符串 - * @param prefixAndSuffix 前置和后置字符 - * @return 去掉包装字符的字符串 - * @since 4.0.1 - */ - public static String unWrap(CharSequence str, char prefixAndSuffix) { - return unWrap(str, prefixAndSuffix, prefixAndSuffix); - } - - /** - * 指定字符串是否被包装 - * - * @param str 字符串 - * @param prefix 前缀 - * @param suffix 后缀 - * @return 是否被包装 - */ - public static boolean isWrap(CharSequence str, String prefix, String suffix) { - if (ArrayUtil.hasNull(str, prefix, suffix)) { - return false; - } - final String str2 = str.toString(); - return str2.startsWith(prefix) && str2.endsWith(suffix); - } - - /** - * 指定字符串是否被同一字符包装(前后都有这些字符串) - * - * @param str 字符串 - * @param wrapper 包装字符串 - * @return 是否被包装 - */ - public static boolean isWrap(CharSequence str, String wrapper) { - return isWrap(str, wrapper, wrapper); - } - - /** - * 指定字符串是否被同一字符包装(前后都有这些字符串) - * - * @param str 字符串 - * @param wrapper 包装字符 - * @return 是否被包装 - */ - public static boolean isWrap(CharSequence str, char wrapper) { - return isWrap(str, wrapper, wrapper); - } - - /** - * 指定字符串是否被包装 - * - * @param str 字符串 - * @param prefixChar 前缀 - * @param suffixChar 后缀 - * @return 是否被包装 - */ - public static boolean isWrap(CharSequence str, char prefixChar, char suffixChar) { - if (null == str) { - return false; - } - - return str.charAt(0) == prefixChar && str.charAt(str.length() - 1) == suffixChar; - } - - /** - * 补充字符串以满足最小长度 - * - *
      -	 * StrUtil.padPre(null, *, *);//null
      -	 * StrUtil.padPre("1", 3, "ABC");//"AB1"
      -	 * StrUtil.padPre("123", 2, "ABC");//"12"
      -	 * 
      - * - * @param str 字符串 - * @param minLength 最小长度 - * @param padStr 补充的字符 - * @return 补充后的字符串 - */ - public static String padPre(CharSequence str, int minLength, CharSequence padStr) { - if (null == str) { - return null; - } - final int strLen = str.length(); - if (strLen == minLength) { - return str.toString(); - } else if (strLen > minLength) { - return subPre(str, minLength); - } - - return repeatByLength(padStr, minLength - strLen).concat(str.toString()); - } - - /** - * 补充字符串以满足最小长度 - * - *
      -	 * StrUtil.padPre(null, *, *);//null
      -	 * StrUtil.padPre("1", 3, '0');//"001"
      -	 * StrUtil.padPre("123", 2, '0');//"12"
      -	 * 
      - * - * @param str 字符串 - * @param minLength 最小长度 - * @param padChar 补充的字符 - * @return 补充后的字符串 - */ - public static String padPre(CharSequence str, int minLength, char padChar) { - if (null == str) { - return null; - } - final int strLen = str.length(); - if (strLen == minLength) { - return str.toString(); - } else if (strLen > minLength) { - return subPre(str, minLength); - } - - return repeat(padChar, minLength - strLen).concat(str.toString()); - } - - /** - * 补充字符串以满足最小长度 - * - *
      -	 * StrUtil.padAfter(null, *, *);//null
      -	 * StrUtil.padAfter("1", 3, '0');//"100"
      -	 * StrUtil.padAfter("123", 2, '0');//"23"
      -	 * 
      - * - * @param str 字符串,如果为{@code null},直接返回null - * @param minLength 最小长度 - * @param padChar 补充的字符 - * @return 补充后的字符串 - */ - public static String padAfter(CharSequence str, int minLength, char padChar) { - if (null == str) { - return null; - } - final int strLen = str.length(); - if (strLen == minLength) { - return str.toString(); - } else if (strLen > minLength) { - return sub(str, strLen - minLength, strLen); - } - - return str.toString().concat(repeat(padChar, minLength - strLen)); - } - - /** - * 补充字符串以满足最小长度 - * - *
      -	 * StrUtil.padAfter(null, *, *);//null
      -	 * StrUtil.padAfter("1", 3, "ABC");//"1AB"
      -	 * StrUtil.padAfter("123", 2, "ABC");//"23"
      -	 * 
      - * - * @param str 字符串,如果为{@code null},直接返回null - * @param minLength 最小长度 - * @param padStr 补充的字符 - * @return 补充后的字符串 - * @since 4.3.2 - */ - public static String padAfter(CharSequence str, int minLength, CharSequence padStr) { - if (null == str) { - return null; - } - final int strLen = str.length(); - if (strLen == minLength) { - return str.toString(); - } else if (strLen > minLength) { - return subSufByLength(str, minLength); - } - - return str.toString().concat(repeatByLength(padStr, minLength - strLen)); - } - - /** - * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 - * - *
      -	 * StrUtil.center(null, *)   = null
      -	 * StrUtil.center("", 4)     = "    "
      -	 * StrUtil.center("ab", -1)  = "ab"
      -	 * StrUtil.center("ab", 4)   = " ab "
      -	 * StrUtil.center("abcd", 2) = "abcd"
      -	 * StrUtil.center("a", 4)    = " a  "
      -	 * 
      - * - * @param str 字符串 - * @param size 指定长度 - * @return 补充后的字符串 - * @since 4.3.2 - */ - public static String center(CharSequence str, final int size) { - return center(str, size, CharUtil.SPACE); - } - - /** - * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 - * - *
      -	 * StrUtil.center(null, *, *)     = null
      -	 * StrUtil.center("", 4, ' ')     = "    "
      -	 * StrUtil.center("ab", -1, ' ')  = "ab"
      -	 * StrUtil.center("ab", 4, ' ')   = " ab "
      -	 * StrUtil.center("abcd", 2, ' ') = "abcd"
      -	 * StrUtil.center("a", 4, ' ')    = " a  "
      -	 * StrUtil.center("a", 4, 'y')   = "yayy"
      -	 * StrUtil.center("abc", 7, ' ')   = "  abc  "
      -	 * 
      - * - * @param str 字符串 - * @param size 指定长度 - * @param padChar 两边补充的字符 - * @return 补充后的字符串 - * @since 4.3.2 - */ - public static String center(CharSequence str, final int size, char padChar) { - if (str == null || size <= 0) { - return str(str); - } - final int strLen = str.length(); - final int pads = size - strLen; - if (pads <= 0) { - return str.toString(); - } - str = padPre(str, strLen + pads / 2, padChar); - str = padAfter(str, size, padChar); - return str.toString(); - } - - /** - * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 - * - *
      -	 * StrUtil.center(null, *, *)     = null
      -	 * StrUtil.center("", 4, " ")     = "    "
      -	 * StrUtil.center("ab", -1, " ")  = "ab"
      -	 * StrUtil.center("ab", 4, " ")   = " ab "
      -	 * StrUtil.center("abcd", 2, " ") = "abcd"
      -	 * StrUtil.center("a", 4, " ")    = " a  "
      -	 * StrUtil.center("a", 4, "yz")   = "yayz"
      -	 * StrUtil.center("abc", 7, null) = "  abc  "
      -	 * StrUtil.center("abc", 7, "")   = "  abc  "
      -	 * 
      - * - * @param str 字符串 - * @param size 指定长度 - * @param padStr 两边补充的字符串 - * @return 补充后的字符串 - */ - public static String center(CharSequence str, final int size, CharSequence padStr) { - if (str == null || size <= 0) { - return str(str); - } - if (isEmpty(padStr)) { - padStr = SPACE; - } - final int strLen = str.length(); - final int pads = size - strLen; - if (pads <= 0) { - return str.toString(); - } - str = padPre(str, strLen + pads / 2, padStr); - str = padAfter(str, size, padStr); - return str.toString(); - } - /** * 创建StringBuilder对象 * @@ -3609,30 +507,6 @@ public class StrUtil { return StrBuilder.create(capacity); } - /** - * 创建StringBuilder对象 - * - * @param strs 初始字符串列表 - * @return StringBuilder对象 - */ - public static StringBuilder builder(CharSequence... strs) { - final StringBuilder sb = new StringBuilder(); - for (CharSequence str : strs) { - sb.append(str); - } - return sb; - } - - /** - * 创建StrBuilder对象 - * - * @param strs 初始字符串列表 - * @return StrBuilder对象 - */ - public static StrBuilder strBuilder(CharSequence... strs) { - return StrBuilder.create(strs); - } - /** * 获得StringReader * @@ -3655,566 +529,6 @@ public class StrUtil { return new StringWriter(); } - /** - * 统计指定内容中包含指定字符串的数量
      - * 参数为 {@code null} 或者 "" 返回 {@code 0}. - * - *
      -	 * StrUtil.count(null, *)       = 0
      -	 * StrUtil.count("", *)         = 0
      -	 * StrUtil.count("abba", null)  = 0
      -	 * StrUtil.count("abba", "")    = 0
      -	 * StrUtil.count("abba", "a")   = 2
      -	 * StrUtil.count("abba", "ab")  = 1
      -	 * StrUtil.count("abba", "xxx") = 0
      -	 * 
      - * - * @param content 被查找的字符串 - * @param strForSearch 需要查找的字符串 - * @return 查找到的个数 - */ - public static int count(CharSequence content, CharSequence strForSearch) { - if (hasEmpty(content, strForSearch) || strForSearch.length() > content.length()) { - return 0; - } - - int count = 0; - int idx = 0; - final String content2 = content.toString(); - final String strForSearch2 = strForSearch.toString(); - while ((idx = content2.indexOf(strForSearch2, idx)) > -1) { - count++; - idx += strForSearch.length(); - } - return count; - } - - /** - * 统计指定内容中包含指定字符的数量 - * - * @param content 内容 - * @param charForSearch 被统计的字符 - * @return 包含数量 - */ - public static int count(CharSequence content, char charForSearch) { - int count = 0; - if (isEmpty(content)) { - return 0; - } - int contentLength = content.length(); - for (int i = 0; i < contentLength; i++) { - if (charForSearch == content.charAt(i)) { - count++; - } - } - return count; - } - - /** - * 将字符串切分为N等份 - * - * @param str 字符串 - * @param partLength 每等份的长度 - * @return 切分后的数组 - * @since 3.0.6 - */ - public static String[] cut(CharSequence str, int partLength) { - if (null == str) { - return null; - } - int len = str.length(); - if (len < partLength) { - return new String[]{str.toString()}; - } - int part = NumberUtil.count(len, partLength); - final String[] array = new String[part]; - - final String str2 = str.toString(); - for (int i = 0; i < part; i++) { - array[i] = str2.substring(i * partLength, (i == part - 1) ? len : (partLength + i * partLength)); - } - return array; - } - - /** - * 将给定字符串,变成 "xxx...xxx" 形式的字符串 - * - * @param str 字符串 - * @param maxLength 最大长度 - * @return 截取后的字符串 - */ - public static String brief(CharSequence str, int maxLength) { - if (null == str) { - return null; - } - if (str.length() <= maxLength) { - return str.toString(); - } - int w = maxLength / 2; - int l = str.length() + 3; - - final String str2 = str.toString(); - return format("{}...{}", str2.substring(0, maxLength - w), str2.substring(l - w)); - } - - /** - * 比较两个字符串,用于排序 - * - *
      -	 * StrUtil.compare(null, null, *)     = 0
      -	 * StrUtil.compare(null , "a", true)  < 0
      -	 * StrUtil.compare(null , "a", false) > 0
      -	 * StrUtil.compare("a", null, true)   > 0
      -	 * StrUtil.compare("a", null, false)  < 0
      -	 * StrUtil.compare("abc", "abc", *)   = 0
      -	 * StrUtil.compare("a", "b", *)       < 0
      -	 * StrUtil.compare("b", "a", *)       > 0
      -	 * StrUtil.compare("a", "B", *)       > 0
      -	 * StrUtil.compare("ab", "abc", *)    < 0
      -	 * 
      - * - * @param str1 字符串1 - * @param str2 字符串2 - * @param nullIsLess {@code null} 值是否排在前(null是否小于非空值) - * @return 排序值。负数:str1 < str2,正数:str1 > str2, 0:str1 == str2 - */ - public static int compare(final CharSequence str1, final CharSequence str2, final boolean nullIsLess) { - if (str1 == str2) { - return 0; - } - if (str1 == null) { - return nullIsLess ? -1 : 1; - } - if (str2 == null) { - return nullIsLess ? 1 : -1; - } - return str1.toString().compareTo(str2.toString()); - } - - /** - * 比较两个字符串,用于排序,大小写不敏感 - * - *
      -	 * StrUtil.compareIgnoreCase(null, null, *)     = 0
      -	 * StrUtil.compareIgnoreCase(null , "a", true)  < 0
      -	 * StrUtil.compareIgnoreCase(null , "a", false) > 0
      -	 * StrUtil.compareIgnoreCase("a", null, true)   > 0
      -	 * StrUtil.compareIgnoreCase("a", null, false)  < 0
      -	 * StrUtil.compareIgnoreCase("abc", "abc", *)   = 0
      -	 * StrUtil.compareIgnoreCase("abc", "ABC", *)   = 0
      -	 * StrUtil.compareIgnoreCase("a", "b", *)       < 0
      -	 * StrUtil.compareIgnoreCase("b", "a", *)       > 0
      -	 * StrUtil.compareIgnoreCase("a", "B", *)       < 0
      -	 * StrUtil.compareIgnoreCase("A", "b", *)       < 0
      -	 * StrUtil.compareIgnoreCase("ab", "abc", *)    < 0
      -	 * 
      - * - * @param str1 字符串1 - * @param str2 字符串2 - * @param nullIsLess {@code null} 值是否排在前(null是否小于非空值) - * @return 排序值。负数:str1 < str2,正数:str1 > str2, 0:str1 == str2 - */ - public static int compareIgnoreCase(CharSequence str1, CharSequence str2, boolean nullIsLess) { - if (str1 == str2) { - return 0; - } - if (str1 == null) { - return nullIsLess ? -1 : 1; - } - if (str2 == null) { - return nullIsLess ? 1 : -1; - } - return str1.toString().compareToIgnoreCase(str2.toString()); - } - - /** - * 比较两个版本
      - * null版本排在最小:即: - * - *
      -	 * StrUtil.compareVersion(null, "v1") < 0
      -	 * StrUtil.compareVersion("v1", "v1")  = 0
      -	 * StrUtil.compareVersion(null, null)   = 0
      -	 * StrUtil.compareVersion("v1", null) > 0
      -	 * StrUtil.compareVersion("1.0.0", "1.0.2") < 0
      -	 * StrUtil.compareVersion("1.0.2", "1.0.2a") < 0
      -	 * StrUtil.compareVersion("1.13.0", "1.12.1c") > 0
      -	 * StrUtil.compareVersion("V0.0.20170102", "V0.0.20170101") > 0
      -	 * 
      - * - * @param version1 版本1 - * @param version2 版本2 - * @return 排序值。负数:version1 < version2,正数:version1 > version2, 0:version1 == version2 - * @since 4.0.2 - */ - public static int compareVersion(CharSequence version1, CharSequence version2) { - return VersionComparator.INSTANCE.compare(str(version1), str(version2)); - } - - /** - * 指定范围内查找指定字符 - * - * @param str 字符串 - * @param searchChar 被查找的字符 - * @return 位置 - */ - public static int indexOf(final CharSequence str, char searchChar) { - return indexOf(str, searchChar, 0); - } - - /** - * 指定范围内查找指定字符 - * - * @param str 字符串 - * @param searchChar 被查找的字符 - * @param start 起始位置,如果小于0,从0开始查找 - * @return 位置 - */ - public static int indexOf(CharSequence str, char searchChar, int start) { - if (str instanceof String) { - return ((String) str).indexOf(searchChar, start); - } else { - return indexOf(str, searchChar, start, -1); - } - } - - /** - * 指定范围内查找指定字符 - * - * @param str 字符串 - * @param searchChar 被查找的字符 - * @param start 起始位置,如果小于0,从0开始查找 - * @param end 终止位置,如果超过str.length()则默认查找到字符串末尾 - * @return 位置 - */ - public static int indexOf(final CharSequence str, char searchChar, int start, int end) { - if (isEmpty(str)) { - return INDEX_NOT_FOUND; - } - final int len = str.length(); - if (start < 0 || start > len) { - start = 0; - } - if (end > len || end < 0) { - end = len; - } - for (int i = start; i < end; i++) { - if (str.charAt(i) == searchChar) { - return i; - } - } - return INDEX_NOT_FOUND; - } - - /** - * 指定范围内查找字符串,忽略大小写
      - * - *
      -	 * StrUtil.indexOfIgnoreCase(null, *, *)          = -1
      -	 * StrUtil.indexOfIgnoreCase(*, null, *)          = -1
      -	 * StrUtil.indexOfIgnoreCase("", "", 0)           = 0
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
      -	 * StrUtil.indexOfIgnoreCase("abc", "", 9)        = -1
      -	 * 
      - * - * @param str 字符串 - * @param searchStr 需要查找位置的字符串 - * @return 位置 - * @since 3.2.1 - */ - public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { - return indexOfIgnoreCase(str, searchStr, 0); - } - - /** - * 指定范围内查找字符串 - * - *
      -	 * StrUtil.indexOfIgnoreCase(null, *, *)          = -1
      -	 * StrUtil.indexOfIgnoreCase(*, null, *)          = -1
      -	 * StrUtil.indexOfIgnoreCase("", "", 0)           = 0
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
      -	 * StrUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
      -	 * StrUtil.indexOfIgnoreCase("abc", "", 9)        = -1
      -	 * 
      - * - * @param str 字符串 - * @param searchStr 需要查找位置的字符串 - * @param fromIndex 起始位置 - * @return 位置 - * @since 3.2.1 - */ - public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { - return indexOf(str, searchStr, fromIndex, true); - } - - /** - * 指定范围内查找字符串 - * - * @param str 字符串 - * @param searchStr 需要查找位置的字符串 - * @param fromIndex 起始位置 - * @param ignoreCase 是否忽略大小写 - * @return 位置 - * @since 3.2.1 - */ - public static int indexOf(final CharSequence str, CharSequence searchStr, int fromIndex, boolean ignoreCase) { - if (str == null || searchStr == null) { - return INDEX_NOT_FOUND; - } - if (fromIndex < 0) { - fromIndex = 0; - } - - final int endLimit = str.length() - searchStr.length() + 1; - if (fromIndex > endLimit) { - return INDEX_NOT_FOUND; - } - if (searchStr.length() == 0) { - return fromIndex; - } - - if (false == ignoreCase) { - // 不忽略大小写调用JDK方法 - return str.toString().indexOf(searchStr.toString(), fromIndex); - } - - for (int i = fromIndex; i < endLimit; i++) { - if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { - return i; - } - } - return INDEX_NOT_FOUND; - } - - /** - * 指定范围内查找字符串,忽略大小写 - * - * @param str 字符串 - * @param searchStr 需要查找位置的字符串 - * @return 位置 - * @since 3.2.1 - */ - public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { - return lastIndexOfIgnoreCase(str, searchStr, str.length()); - } - - /** - * 指定范围内查找字符串,忽略大小写
      - * fromIndex 为搜索起始位置,从后往前计数 - * - * @param str 字符串 - * @param searchStr 需要查找位置的字符串 - * @param fromIndex 起始位置,从后往前计数 - * @return 位置 - * @since 3.2.1 - */ - public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { - return lastIndexOf(str, searchStr, fromIndex, true); - } - - /** - * 指定范围内查找字符串
      - * fromIndex 为搜索起始位置,从后往前计数 - * - * @param str 字符串 - * @param searchStr 需要查找位置的字符串 - * @param fromIndex 起始位置,从后往前计数 - * @param ignoreCase 是否忽略大小写 - * @return 位置 - * @since 3.2.1 - */ - public static int lastIndexOf(final CharSequence str, final CharSequence searchStr, int fromIndex, boolean ignoreCase) { - if (str == null || searchStr == null) { - return INDEX_NOT_FOUND; - } - if (fromIndex < 0) { - fromIndex = 0; - } - fromIndex = Math.min(fromIndex, str.length()); - - if (searchStr.length() == 0) { - return fromIndex; - } - - if (false == ignoreCase) { - // 不忽略大小写调用JDK方法 - return str.toString().lastIndexOf(searchStr.toString(), fromIndex); - } - - for (int i = fromIndex; i >= 0; i--) { - if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { - return i; - } - } - return INDEX_NOT_FOUND; - } - - /** - * 返回字符串 searchStr 在字符串 str 中第 ordinal 次出现的位置。 - * - *

      - * 如果 str=null 或 searchStr=null 或 ordinal≥0 则返回-1
      - * 此方法来自:Apache-Commons-Lang - *

      - * 例子(*代表任意字符): - * - *

      -	 * StrUtil.ordinalIndexOf(null, *, *)          = -1
      -	 * StrUtil.ordinalIndexOf(*, null, *)          = -1
      -	 * StrUtil.ordinalIndexOf("", "", *)           = 0
      -	 * StrUtil.ordinalIndexOf("aabaabaa", "a", 1)  = 0
      -	 * StrUtil.ordinalIndexOf("aabaabaa", "a", 2)  = 1
      -	 * StrUtil.ordinalIndexOf("aabaabaa", "b", 1)  = 2
      -	 * StrUtil.ordinalIndexOf("aabaabaa", "b", 2)  = 5
      -	 * StrUtil.ordinalIndexOf("aabaabaa", "ab", 1) = 1
      -	 * StrUtil.ordinalIndexOf("aabaabaa", "ab", 2) = 4
      -	 * StrUtil.ordinalIndexOf("aabaabaa", "", 1)   = 0
      -	 * StrUtil.ordinalIndexOf("aabaabaa", "", 2)   = 0
      -	 * 
      - * - * @param str 被检查的字符串,可以为null - * @param searchStr 被查找的字符串,可以为null - * @param ordinal 第几次出现的位置 - * @return 查找到的位置 - * @since 3.2.3 - */ - public static int ordinalIndexOf(String str, String searchStr, int ordinal) { - if (str == null || searchStr == null || ordinal <= 0) { - return INDEX_NOT_FOUND; - } - if (searchStr.length() == 0) { - return 0; - } - int found = 0; - int index = INDEX_NOT_FOUND; - do { - index = str.indexOf(searchStr, index + 1); - if (index < 0) { - return index; - } - found++; - } while (found < ordinal); - return index; - } - - // ------------------------------------------------------------------------------------------------------------------ Append and prepend - - /** - * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串
      - * 不忽略大小写 - * - * @param str 被检查的字符串 - * @param suffix 需要添加到结尾的字符串 - * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 - * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 - * @since 3.0.7 - */ - public static String appendIfMissing(final CharSequence str, final CharSequence suffix, final CharSequence... suffixes) { - return appendIfMissing(str, suffix, false, suffixes); - } - - /** - * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串
      - * 忽略大小写 - * - * @param str 被检查的字符串 - * @param suffix 需要添加到结尾的字符串 - * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 - * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 - * @since 3.0.7 - */ - public static String appendIfMissingIgnoreCase(final CharSequence str, final CharSequence suffix, final CharSequence... suffixes) { - return appendIfMissing(str, suffix, true, suffixes); - } - - /** - * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串 - * - * @param str 被检查的字符串 - * @param suffix 需要添加到结尾的字符串 - * @param ignoreCase 检查结尾时是否忽略大小写 - * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 - * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 - * @since 3.0.7 - */ - public static String appendIfMissing(final CharSequence str, final CharSequence suffix, final boolean ignoreCase, final CharSequence... suffixes) { - if (str == null || isEmpty(suffix) || endWith(str, suffix, ignoreCase)) { - return str(str); - } - if (suffixes != null && suffixes.length > 0) { - for (final CharSequence s : suffixes) { - if (endWith(str, s, ignoreCase)) { - return str.toString(); - } - } - } - return str.toString().concat(suffix.toString()); - } - - /** - * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串
      - * 不忽略大小写 - * - * @param str 被检查的字符串 - * @param prefix 需要添加到首部的字符串 - * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 - * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 - * @since 3.0.7 - */ - public static String prependIfMissing(final CharSequence str, final CharSequence prefix, final CharSequence... prefixes) { - return prependIfMissing(str, prefix, false, prefixes); - } - - /** - * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串
      - * 忽略大小写 - * - * @param str 被检查的字符串 - * @param prefix 需要添加到首部的字符串 - * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 - * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 - * @since 3.0.7 - */ - public static String prependIfMissingIgnoreCase(final CharSequence str, final CharSequence prefix, final CharSequence... prefixes) { - return prependIfMissing(str, prefix, true, prefixes); - } - - /** - * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串 - * - * @param str 被检查的字符串 - * @param prefix 需要添加到首部的字符串 - * @param ignoreCase 检查结尾时是否忽略大小写 - * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 - * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 - * @since 3.0.7 - */ - public static String prependIfMissing(final CharSequence str, final CharSequence prefix, final boolean ignoreCase, final CharSequence... prefixes) { - if (str == null || isEmpty(prefix) || startWith(str, prefix, ignoreCase)) { - return str(str); - } - if (prefixes != null && prefixes.length > 0) { - for (final CharSequence s : prefixes) { - if (startWith(str, s, ignoreCase)) { - return str.toString(); - } - } - } - return prefix.toString().concat(str.toString()); - } - /** * 反转字符串
      * 例如:abcd =》dcba @@ -4227,6 +541,8 @@ public class StrUtil { return new String(ArrayUtil.reverse(str.toCharArray())); } + // ------------------------------------------------------------------------ fill + /** * 将已有字符串填充为规定长度,如果已有字符串超过这个长度则返回这个字符串
      * 字符填充于字符串前 @@ -4275,258 +591,6 @@ public class StrUtil { return isPre ? filledStr.concat(str) : str.concat(filledStr); } - /** - * 截取两个字符串的不同部分(长度一致),判断截取的子串是否相同
      - * 任意一个字符串为null返回false - * - * @param str1 第一个字符串 - * @param start1 第一个字符串开始的位置 - * @param str2 第二个字符串 - * @param start2 第二个字符串开始的位置 - * @param length 截取长度 - * @param ignoreCase 是否忽略大小写 - * @return 子串是否相同 - * @since 3.2.1 - */ - public static boolean isSubEquals(CharSequence str1, int start1, CharSequence str2, int start2, int length, boolean ignoreCase) { - if (null == str1 || null == str2) { - return false; - } - - return str1.toString().regionMatches(ignoreCase, start1, str2.toString(), start2, length); - } - - /** - * 字符串的每一个字符是否都与定义的匹配器匹配 - * - * @param value 字符串 - * @param matcher 匹配器 - * @return 是否全部匹配 - * @since 3.2.3 - */ - public static boolean isAllCharMatch(CharSequence value, Matcher matcher) { - if (StrUtil.isBlank(value)) { - return false; - } - int len = value.length(); - for (int i = 0; i < len; i++) { - if (false == matcher.match(value.charAt(i))) { - return false; - } - } - return true; - } - - /** - * 替换字符串中的指定字符串,忽略大小写 - * - * @param str 字符串 - * @param searchStr 被查找的字符串 - * @param replacement 被替换的字符串 - * @return 替换后的字符串 - * @since 4.0.3 - */ - public static String replaceIgnoreCase(CharSequence str, CharSequence searchStr, CharSequence replacement) { - return replace(str, 0, searchStr, replacement, true); - } - - /** - * 替换字符串中的指定字符串 - * - * @param str 字符串 - * @param searchStr 被查找的字符串 - * @param replacement 被替换的字符串 - * @return 替换后的字符串 - * @since 4.0.3 - */ - public static String replace(CharSequence str, CharSequence searchStr, CharSequence replacement) { - return replace(str, 0, searchStr, replacement, false); - } - - /** - * 替换字符串中的指定字符串 - * - * @param str 字符串 - * @param searchStr 被查找的字符串 - * @param replacement 被替换的字符串 - * @param ignoreCase 是否忽略大小写 - * @return 替换后的字符串 - * @since 4.0.3 - */ - public static String replace(CharSequence str, CharSequence searchStr, CharSequence replacement, boolean ignoreCase) { - return replace(str, 0, searchStr, replacement, ignoreCase); - } - - /** - * 替换字符串中的指定字符串 - * - * @param str 字符串 - * @param fromIndex 开始位置(包括) - * @param searchStr 被查找的字符串 - * @param replacement 被替换的字符串 - * @param ignoreCase 是否忽略大小写 - * @return 替换后的字符串 - * @since 4.0.3 - */ - public static String replace(CharSequence str, int fromIndex, CharSequence searchStr, CharSequence replacement, boolean ignoreCase) { - if (isEmpty(str) || isEmpty(searchStr)) { - return str(str); - } - if (null == replacement) { - replacement = EMPTY; - } - - final int strLength = str.length(); - final int searchStrLength = searchStr.length(); - if (fromIndex > strLength) { - return str(str); - } else if (fromIndex < 0) { - fromIndex = 0; - } - - final StrBuilder result = StrBuilder.create(strLength + 16); - if (0 != fromIndex) { - result.append(str.subSequence(0, fromIndex)); - } - - int preIndex = fromIndex; - int index; - while ((index = indexOf(str, searchStr, preIndex, ignoreCase)) > -1) { - result.append(str.subSequence(preIndex, index)); - result.append(replacement); - preIndex = index + searchStrLength; - } - - if (preIndex < strLength) { - // 结尾部分 - result.append(str.subSequence(preIndex, strLength)); - } - return result.toString(); - } - - /** - * 替换指定字符串的指定区间内字符为固定字符 - * - * @param str 字符串 - * @param startInclude 开始位置(包含) - * @param endExclude 结束位置(不包含) - * @param replacedChar 被替换的字符 - * @return 替换后的字符串 - * @since 3.2.1 - */ - public static String replace(CharSequence str, int startInclude, int endExclude, char replacedChar) { - if (isEmpty(str)) { - return str(str); - } - final int strLength = str.length(); - if (startInclude > strLength) { - return str(str); - } - if (endExclude > strLength) { - endExclude = strLength; - } - if (startInclude > endExclude) { - // 如果起始位置大于结束位置,不替换 - return str(str); - } - - final char[] chars = new char[strLength]; - for (int i = 0; i < strLength; i++) { - if (i >= startInclude && i < endExclude) { - chars[i] = replacedChar; - } else { - chars[i] = str.charAt(i); - } - } - return new String(chars); - } - - /** - * 替换所有正则匹配的文本,并使用自定义函数决定如何替换 - * - * @param str 要替换的字符串 - * @param pattern 用于匹配的正则式 - * @param replaceFun 决定如何替换的函数 - * @return 替换后的字符串 - * @see ReUtil#replaceAll(CharSequence, Pattern, Func1) - * @since 4.2.2 - */ - public static String replace(CharSequence str, Pattern pattern, Func1 replaceFun) { - return ReUtil.replaceAll(str, pattern, replaceFun); - } - - /** - * 替换所有正则匹配的文本,并使用自定义函数决定如何替换 - * - * @param str 要替换的字符串 - * @param regex 用于匹配的正则式 - * @param replaceFun 决定如何替换的函数 - * @return 替换后的字符串 - * @see ReUtil#replaceAll(CharSequence, String, Func1) - * @since 4.2.2 - */ - public static String replace(CharSequence str, String regex, Func1 replaceFun) { - return ReUtil.replaceAll(str, regex, replaceFun); - } - - /** - * 替换指定字符串的指定区间内字符为"*" - * - * @param str 字符串 - * @param startInclude 开始位置(包含) - * @param endExclude 结束位置(不包含) - * @return 替换后的字符串 - * @since 4.1.14 - */ - public static String hide(CharSequence str, int startInclude, int endExclude) { - return replace(str, startInclude, endExclude, '*'); - } - - /** - * 替换字符字符数组中所有的字符为replacedStr
      - * 提供的chars为所有需要被替换的字符,例如:"\r\n",则"\r"和"\n"都会被替换,哪怕他们单独存在 - * - * @param str 被检查的字符串 - * @param chars 需要替换的字符列表,用一个字符串表示这个字符列表 - * @param replacedStr 替换成的字符串 - * @return 新字符串 - * @since 3.2.2 - */ - public static String replaceChars(CharSequence str, String chars, CharSequence replacedStr) { - if (isEmpty(str) || isEmpty(chars)) { - return str(str); - } - return replaceChars(str, chars.toCharArray(), replacedStr); - } - - /** - * 替换字符字符数组中所有的字符为replacedStr - * - * @param str 被检查的字符串 - * @param chars 需要替换的字符列表 - * @param replacedStr 替换成的字符串 - * @return 新字符串 - * @since 3.2.2 - */ - public static String replaceChars(CharSequence str, char[] chars, CharSequence replacedStr) { - if (isEmpty(str) || ArrayUtil.isEmpty(chars)) { - return str(str); - } - - final Set set = new HashSet<>(chars.length); - for (char c : chars) { - set.add(c); - } - int strLen = str.length(); - final StringBuilder builder = builder(); - char c; - for (int i = 0; i < strLen; i++) { - c = str.charAt(i); - builder.append(set.contains(c) ? replacedStr : c); - } - return builder.toString(); - } - /** * 计算两个字符串的相似度 * @@ -4552,81 +616,6 @@ public class StrUtil { return TextSimilarity.similar(str1, str2, scale); } - /** - * 字符串指定位置的字符是否与给定字符相同
      - * 如果字符串为null,返回false
      - * 如果给定的位置大于字符串长度,返回false
      - * 如果给定的位置小于0,返回false - * - * @param str 字符串 - * @param position 位置 - * @param c 需要对比的字符 - * @return 字符串指定位置的字符是否与给定字符相同 - * @since 3.3.1 - */ - public static boolean equalsCharAt(CharSequence str, int position, char c) { - if (null == str || position < 0) { - return false; - } - return str.length() > position && c == str.charAt(position); - } - - /** - * 给定字符串数组的总长度
      - * null字符长度定义为0 - * - * @param strs 字符串数组 - * @return 总长度 - * @since 4.0.1 - */ - public static int totalLength(CharSequence... strs) { - int totalLength = 0; - for (CharSequence str : strs) { - totalLength += (null == str ? 0 : str.length()); - } - return totalLength; - } - - /** - * 循环位移指定位置的字符串为指定距离
      - * 当moveLength大于0向右位移,小于0向左位移,0不位移
      - * 当moveLength大于字符串长度时采取循环位移策略,即位移到头后从头(尾)位移,例如长度为10,位移13则表示位移3 - * - * @param str 字符串 - * @param startInclude 起始位置(包括) - * @param endExclude 结束位置(不包括) - * @param moveLength 移动距离,负数表示左移,正数为右移 - * @return 位移后的字符串 - * @since 4.0.7 - */ - public static String move(CharSequence str, int startInclude, int endExclude, int moveLength) { - if (isEmpty(str)) { - return str(str); - } - int len = str.length(); - if (Math.abs(moveLength) > len) { - // 循环位移,当越界时循环 - moveLength = moveLength % len; - } - final StrBuilder strBuilder = StrBuilder.create(len); - if (moveLength > 0) { - int endAfterMove = Math.min(endExclude + moveLength, str.length()); - strBuilder.append(str.subSequence(0, startInclude))// - .append(str.subSequence(endExclude, endAfterMove))// - .append(str.subSequence(startInclude, endExclude))// - .append(str.subSequence(endAfterMove, str.length())); - } else if (moveLength < 0) { - int startAfterMove = Math.max(startInclude + moveLength, 0); - strBuilder.append(str.subSequence(0, startAfterMove))// - .append(str.subSequence(startInclude, endExclude))// - .append(str.subSequence(startAfterMove, startInclude))// - .append(str.subSequence(endExclude, str.length())); - } else { - return str(str); - } - return strBuilder.toString(); - } - /** * 生成随机UUID * @@ -4639,190 +628,44 @@ public class StrUtil { } /** - * 连接多个字符串为一个 + * 格式化文本,使用 {varName} 占位
      + * map = {a: "aValue", b: "bValue"} format("{a} and {b}", map) ---=》 aValue and bValue * - * @param isNullToEmpty 是否null转为"" - * @param strs 字符串数组 - * @return 连接后的字符串 - * @since 4.1.0 + * @param template 文本模板,被替换的部分用 {key} 表示 + * @param map 参数值对 + * @return 格式化后的文本 */ - public static String concat(boolean isNullToEmpty, CharSequence... strs) { - final StrBuilder sb = new StrBuilder(); - for (CharSequence str : strs) { - sb.append(isNullToEmpty ? nullToEmpty(str) : str); - } - return sb.toString(); + public static String format(CharSequence template, Map map) { + return format(template, map, true); } /** - * 给定字符串中的字母是否全部为大写,判断依据如下: + * 格式化文本,使用 {varName} 占位
      + * map = {a: "aValue", b: "bValue"} format("{a} and {b}", map) ---=》 aValue and bValue * - *
      -	 * 1. 大写字母包括A-Z
      -	 * 2. 其它非字母的Unicode符都算作大写
      -	 * 
      - * - * @param str 被检查的字符串 - * @return 是否全部为大写 - * @since 4.2.2 + * @param template 文本模板,被替换的部分用 {key} 表示 + * @param map 参数值对 + * @param ignoreNull 是否忽略 {@code null} 值,忽略则 {@code null} 值对应的变量不被替换,否则替换为"" + * @return 格式化后的文本 + * @since 5.4.3 */ - public static boolean isUpperCase(CharSequence str) { - if (null == str) { - return false; + public static String format(CharSequence template, Map map, boolean ignoreNull) { + if (null == template) { + return null; } - final int len = str.length(); - for (int i = 0; i < len; i++) { - if (Character.isLowerCase(str.charAt(i))) { - return false; + if (null == map || map.isEmpty()) { + return template.toString(); + } + + String template2 = template.toString(); + String value; + for (Map.Entry entry : map.entrySet()) { + value = utf8Str(entry.getValue()); + if (null == value && ignoreNull) { + continue; } + template2 = replace(template2, "{" + entry.getKey() + "}", value); } - return true; - } - - /** - * 给定字符串中的字母是否全部为小写,判断依据如下: - * - *
      -	 * 1. 小写字母包括a-z
      -	 * 2. 其它非字母的Unicode符都算作小写
      -	 * 
      - * - * @param str 被检查的字符串 - * @return 是否全部为小写 - * @since 4.2.2 - */ - public static boolean isLowerCase(CharSequence str) { - if (null == str) { - return false; - } - final int len = str.length(); - for (int i = 0; i < len; i++) { - if (Character.isUpperCase(str.charAt(i))) { - return false; - } - } - return true; - } - - /** - * 获取字符串的长度,如果为null返回0 - * - * @param cs a 字符串 - * @return 字符串的长度,如果为null返回0 - * @since 4.3.2 - */ - public static int length(CharSequence cs) { - return cs == null ? 0 : cs.length(); - } - - /** - * 给定字符串转为bytes后的byte数(byte长度) - * - * @param cs 字符串 - * @param charset 编码 - * @return byte长度 - * @since 4.5.2 - */ - public static int byteLength(CharSequence cs, Charset charset) { - return cs == null ? 0 : cs.toString().getBytes(charset).length; - } - - /** - * 切换给定字符串中的大小写。大写转小写,小写转大写。 - * - *
      -	 * StrUtil.swapCase(null)                 = null
      -	 * StrUtil.swapCase("")                   = ""
      -	 * StrUtil.swapCase("The dog has a BONE") = "tHE DOG HAS A bone"
      -	 * 
      - * - * @param str 字符串 - * @return 交换后的字符串 - * @since 4.3.2 - */ - public static String swapCase(final String str) { - if (isEmpty(str)) { - return str; - } - - final char[] buffer = str.toCharArray(); - - for (int i = 0; i < buffer.length; i++) { - final char ch = buffer[i]; - if (Character.isUpperCase(ch)) { - buffer[i] = Character.toLowerCase(ch); - } else if (Character.isTitleCase(ch)) { - buffer[i] = Character.toLowerCase(ch); - } else if (Character.isLowerCase(ch)) { - buffer[i] = Character.toUpperCase(ch); - } - } - return new String(buffer); - } - - /** - * 过滤字符串 - * - * @param str 字符串 - * @param filter 过滤器 - * @return 过滤后的字符串 - * @since 5.4.0 - */ - public static String filter(CharSequence str, final Filter filter) { - if (str == null || filter == null) { - return str(str); - } - - int len = str.length(); - final StringBuilder sb = new StringBuilder(len); - char c; - for (int i = 0; i < len; i++) { - c = str.charAt(i); - if (filter.accept(c)) { - sb.append(c); - } - } - return sb.toString(); - } - - /** - * 返回第一个非{@code null} 元素 - * - * @param strs 多个元素 - * @param 元素类型 - * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} - * @since 5.4.1 - */ - @SuppressWarnings("unchecked") - public T firstNonNull(T... strs) { - return ArrayUtil.firstNonNull(strs); - } - - /** - * 返回第一个非empty 元素 - * - * @param strs 多个元素 - * @param 元素类型 - * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} - * @see #isNotEmpty(CharSequence) - * @since 5.4.1 - */ - @SuppressWarnings("unchecked") - public T firstNonEmpty(T... strs) { - return ArrayUtil.firstMatch(StrUtil::isNotEmpty, strs); - } - - /** - * 返回第一个非blank 元素 - * - * @param strs 多个元素 - * @param 元素类型 - * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} - * @see #isNotBlank(CharSequence) - * @since 5.4.1 - */ - @SuppressWarnings("unchecked") - public T firstNonBlank(T... strs) { - return ArrayUtil.firstMatch(StrUtil::isNotBlank, strs); + return template2; } } diff --git a/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java index fd200cb6b..ef3609771 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java @@ -65,6 +65,36 @@ import java.util.Map; */ public class XmlUtil { + /** + * 字符串常量:XML 空格转义 {@code " " -> " "} + */ + public static final String NBSP = " "; + + /** + * 字符串常量:XML And 符转义 {@code "&" -> "&"} + */ + public static final String AMP = "&"; + + /** + * 字符串常量:XML 双引号转义 {@code """ -> "\""} + */ + public static final String QUOTE = """; + + /** + * 字符串常量:XML 单引号转义 {@code "&apos" -> "'"} + */ + public static final String APOS = "'"; + + /** + * 字符串常量:XML 小于号转义 {@code "<" -> "<"} + */ + public static final String LT = "<"; + + /** + * 字符串常量:XML 大于号转义 {@code ">" -> ">"} + */ + public static final String GT = ">"; + /** * 在XML中无效的字符 正则 */ diff --git a/hutool-extra/src/main/java/cn/hutool/extra/spring/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/spring/package-info.java new file mode 100644 index 000000000..f9652b68e --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/spring/package-info.java @@ -0,0 +1,7 @@ +/** + * Spring相关工具封装 + * + * @author looly + * + */ +package cn.hutool.extra.spring; \ No newline at end of file diff --git a/hutool-extra/src/test/java/cn/hutool/extra/spring/EnableSprintUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/spring/EnableSpringUtilTest.java similarity index 56% rename from hutool-extra/src/test/java/cn/hutool/extra/spring/EnableSprintUtilTest.java rename to hutool-extra/src/test/java/cn/hutool/extra/spring/EnableSpringUtilTest.java index cf202dc8d..5d48569c0 100644 --- a/hutool-extra/src/test/java/cn/hutool/extra/spring/EnableSprintUtilTest.java +++ b/hutool-extra/src/test/java/cn/hutool/extra/spring/EnableSpringUtilTest.java @@ -10,15 +10,15 @@ import org.springframework.test.context.junit4.SpringRunner; * @author sidian */ @RunWith(SpringRunner.class) -@SpringBootTest(classes = EnableSprintUtilTest.class) +@SpringBootTest(classes = EnableSpringUtilTest.class) @EnableSpringUtil -public class EnableSprintUtilTest { +public class EnableSpringUtilTest { - @Test - public void test() { - // 使用@EnableSpringUtil注解后, 能获取上下文 - Assert.assertNotNull(SpringUtil.getApplicationContext()); - // 不使用时, 为null + @Test + public void test() { + // 使用@EnableSpringUtil注解后, 能获取上下文 + Assert.assertNotNull(SpringUtil.getApplicationContext()); + // 不使用时, 为null // Assert.assertNull(SpringUtil.getApplicationContext()); - } + } } From 68da52bf827da3da59abf10eb4142a8867602ac8 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 8 Dec 2020 06:23:48 +0800 Subject: [PATCH 26/47] fix code --- hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java index 7fb068a01..94aa03ce5 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java @@ -239,7 +239,7 @@ public class StrBuilder implements CharSequence, Appendable, Serializable { */ public StrBuilder insert(int index, CharSequence csq) { if (null == csq) { - csq = "null"; + csq = StrUtil.EMPTY; } int len = csq.length(); moveDataAfterIndex(index, csq.length()); @@ -524,7 +524,7 @@ public class StrBuilder implements CharSequence, Appendable, Serializable { */ private void ensureCapacity(int minimumCapacity) { // overflow-conscious code - if (minimumCapacity - value.length < 0) { + if (minimumCapacity - value.length > 0) { expandCapacity(minimumCapacity); } } From 30e1eb929c218335d35fa7965f546eb9986c172b Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 9 Dec 2020 00:08:12 +0800 Subject: [PATCH 27/47] fix bugs --- CHANGELOG.md | 2 + .../main/java/cn/hutool/db/sql/Wrapper.java | 3 +- .../poi/excel/sax/Excel07SaxReader.java | 42 ++++++++++++++++--- .../cn/hutool/poi/excel/ExcelSaxReadTest.java | 8 ++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65dd28373..d45f0d7e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ * 【core 】 修复TemporalAccessorUtil无法格式化LocalDate带时间问题(issue#1289@Github) * 【json 】 修复自定义日期格式的LocalDateTime没有包装引号问题(issue#1289@Github) * 【cache 】 get中unlock改为unlockRead(issue#1294@Github) +* 【db 】 修复表名包含点导致的问题(issue#1300@Github) +* 【poi 】 修复xdr:row标签导致的问题(issue#1297@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java b/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java index 525fcc5a2..4f6f8d4f8 100644 --- a/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java +++ b/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java @@ -1,5 +1,6 @@ package cn.hutool.db.sql; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Editor; import cn.hutool.core.util.ArrayUtil; @@ -98,7 +99,7 @@ public class Wrapper { //对于Oracle这类数据库,表名中包含用户名需要单独拆分包装 if(field.contains(StrUtil.DOT)){ - final Collection target = CollectionUtil.filter(StrUtil.split(field, StrUtil.C_DOT), (Editor) t -> StrUtil.format("{}{}{}", preWrapQuote, t, sufWrapQuote)); + final Collection target = CollUtil.filter(StrUtil.split(field, StrUtil.C_DOT, 2), (Editor) t -> StrUtil.format("{}{}{}", preWrapQuote, t, sufWrapQuote)); return CollectionUtil.join(target, StrUtil.DOT); } diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java index 2fb116468..9ed1ca7aa 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java @@ -66,6 +66,8 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader 0) { - addCellValue(curCell++, ""); + addCellValue(curCell++, StrUtil.EMPTY); } } } diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java index 3407be109..4b07c60af 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java @@ -175,4 +175,12 @@ public class ExcelSaxReadTest { ExcelUtil.getReader(file).read().forEach(Console::log); } + + @Test + @Ignore + public void readXlsmTest(){ + ExcelUtil.readBySax("d:/test/WhiteListTemplate.xlsm", -1, (sheetIndex, rowIndex, rowlist) -> { + Console.log("[{}] [{}] {}", sheetIndex, rowIndex, rowlist); + }); + } } From 7d4838a0a4b44e8bf92c8d98c87a56108445c0e5 Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 9 Dec 2020 00:37:23 +0800 Subject: [PATCH 28/47] add SheetDataSaxHandler --- CHANGELOG.md | 1 + .../poi/excel/sax/Excel07SaxReader.java | 305 +---------------- .../poi/excel/sax/SheetDataSaxHandler.java | 307 ++++++++++++++++++ 3 files changed, 324 insertions(+), 289 deletions(-) create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetDataSaxHandler.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d45f0d7e7..0652a9506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * 【dfa 】 增加FoundWord(pr#1290@Github) * 【core 】 增加Segment(pr#1290@Github) * 【core 】 增加CharSequenceUtil +* 【poi 】 Excel07SaxReader拆分出SheetDataSaxHandler ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java index 9ed1ca7aa..4c55b90d2 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java @@ -2,30 +2,19 @@ package cn.hutool.poi.excel.sax; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; -import cn.hutool.core.text.StrBuilder; import cn.hutool.core.util.NumberUtil; -import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.poi.excel.cell.FormulaCellValue; import cn.hutool.poi.excel.sax.handler.RowHandler; import cn.hutool.poi.exceptions.POIException; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.exceptions.OpenXML4JException; import org.apache.poi.openxml4j.opc.OPCPackage; -import org.apache.poi.ss.usermodel.BuiltinFormats; import org.apache.poi.xssf.eventusermodel.XSSFReader; -import org.apache.poi.xssf.model.SharedStringsTable; -import org.apache.poi.xssf.model.StylesTable; -import org.apache.poi.xssf.usermodel.XSSFCellStyle; -import org.xml.sax.Attributes; -import org.xml.sax.helpers.DefaultHandler; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.Iterator; -import java.util.List; /** * Sax方式读取Excel文件
      @@ -34,52 +23,11 @@ import java.util.List; * @author Looly * @since 3.1.2 */ -public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader { +public class Excel07SaxReader implements ExcelSaxReader { // sheet r:Id前缀 private static final String RID_PREFIX = "rId"; - - // 单元格的格式表,对应style.xml - private StylesTable stylesTable; - // excel 2007 的共享字符串表,对应sharedString.xml - private SharedStringsTable sharedStringsTable; - // sheet的索引 - private int sheetIndex; - - // 当前非空行 - private int index; - // 当前列 - private int curCell; - // 单元数据类型 - private CellDataType cellDataType; - // 当前行号,从0开始 - private long rowNumber; - // 当前列坐标, 如A1,B5 - private String curCoordinate; - // 当前节点名称 - private ElementName curElementName; - // 前一个列的坐标 - private String preCoordinate; - // 行的最大列坐标 - private String maxCellCoordinate; - // 单元格样式 - private XSSFCellStyle xssfCellStyle; - // 单元格存储的格式化字符串,nmtFmt的formatCode属性的值 - private String numFmtString; - // 是否处于sheetData标签内,sax只解析此标签内的内容,其它标签忽略 - private boolean isInSheetData; - - // 上一次的内容 - private final StrBuilder lastContent = StrUtil.strBuilder(); - // 上一次的内容 - private final StrBuilder lastFormula = StrUtil.strBuilder(); - // 存储每行的列元素 - private List rowCellList = new ArrayList<>(); - - /** - * 行处理器 - */ - private RowHandler rowHandler; + private final SheetDataSaxHandler handler; /** * 构造 @@ -87,7 +35,7 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader -1) { + if (this.handler.sheetIndex > -1) { // 根据 rId# 或 rSheet# 查找sheet - sheetInputStream = xssfReader.getSheet(RID_PREFIX + (this.sheetIndex + 1)); - ExcelSaxUtil.readFrom(sheetInputStream, this); - rowHandler.doAfterAllAnalysed(); + sheetInputStream = xssfReader.getSheet(RID_PREFIX + (this.handler.sheetIndex + 1)); + ExcelSaxUtil.readFrom(sheetInputStream, this.handler); + this.handler.rowHandler.doAfterAllAnalysed(); } else { - this.sheetIndex = -1; + this.handler.sheetIndex = -1; // 遍历所有sheet final Iterator sheetInputStreams = xssfReader.getSheetsData(); while (sheetInputStreams.hasNext()) { // 重新读取一个sheet时行归零 - index = 0; - this.sheetIndex++; + this.handler.index = 0; + this.handler.sheetIndex++; sheetInputStream = sheetInputStreams.next(); - ExcelSaxUtil.readFrom(sheetInputStream, this); - rowHandler.doAfterAllAnalysed(); + ExcelSaxUtil.readFrom(sheetInputStream, this.handler); + this.handler.rowHandler.doAfterAllAnalysed(); } } } catch (RuntimeException e) { @@ -325,143 +190,5 @@ public class Excel07SaxReader extends DefaultHandler implements ExcelSaxReader(curCell + 1); - // 行数增加 - index++; - // 当前列置0 - curCell = 0; - // 置空当前列坐标和前一列坐标 - curCoordinate = null; - preCoordinate = null; - } - - /** - * 在一行中的指定列增加值 - * - * @param index 位置 - * @param value 值 - */ - private void addCellValue(int index, Object value) { - this.rowCellList.add(index, value); - this.rowHandler.handleCell(this.sheetIndex, this.rowNumber, index, value, this.xssfCellStyle); - } - - /** - * 填充空白单元格,如果前一个单元格大于后一个,不需要填充
      - * - * @param preCoordinate 前一个单元格坐标 - * @param curCoordinate 当前单元格坐标 - * @param isEnd 是否为最后一个单元格 - */ - private void fillBlankCell(String preCoordinate, String curCoordinate, boolean isEnd) { - if (false == curCoordinate.equals(preCoordinate)) { - int len = ExcelSaxUtil.countNullCell(preCoordinate, curCoordinate); - if (isEnd) { - len++; - } - while (len-- > 0) { - addCellValue(curCell++, StrUtil.EMPTY); - } - } - } - - /** - * 设置单元格的类型 - * - * @param attributes 属性 - */ - private void setCellType(Attributes attributes) { - // numFmtString的值 - numFmtString = StrUtil.EMPTY; - this.cellDataType = CellDataType.of(AttributeName.t.getValue(attributes)); - - // 获取单元格的xf索引,对应style.xml中cellXfs的子元素xf - if (null != this.stylesTable) { - final String xfIndexStr = AttributeName.s.getValue(attributes); - if (null != xfIndexStr) { - this.xssfCellStyle = stylesTable.getStyleAt(Integer.parseInt(xfIndexStr)); - // 单元格存储格式的索引,对应style.xml中的numFmts元素的子元素索引 - final int numFmtIndex = xssfCellStyle.getDataFormat(); - this.numFmtString = ObjectUtil.defaultIfNull( - xssfCellStyle.getDataFormatString(), - BuiltinFormats.getBuiltinFormat(numFmtIndex)); - if (CellDataType.NUMBER == this.cellDataType && ExcelSaxUtil.isDateFormat(numFmtIndex, numFmtString)) { - cellDataType = CellDataType.DATE; - } - } - } - - } // --------------------------------------------------------------------------------------- Private method end } \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetDataSaxHandler.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetDataSaxHandler.java new file mode 100644 index 000000000..b6d63ee7b --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetDataSaxHandler.java @@ -0,0 +1,307 @@ +package cn.hutool.poi.excel.sax; + +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.cell.FormulaCellValue; +import cn.hutool.poi.excel.sax.handler.RowHandler; +import org.apache.poi.ss.usermodel.BuiltinFormats; +import org.apache.poi.xssf.model.SharedStringsTable; +import org.apache.poi.xssf.model.StylesTable; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.xml.sax.Attributes; +import org.xml.sax.helpers.DefaultHandler; + +import java.util.ArrayList; +import java.util.List; + +/** + * sheetData标签内容读取处理器 + * + *
      + * <sheetData></sheetData>
      + * 
      + * @since 5.5.3 + */ +public class SheetDataSaxHandler extends DefaultHandler { + + // 单元格的格式表,对应style.xml + protected StylesTable stylesTable; + // excel 2007 的共享字符串表,对应sharedString.xml + protected SharedStringsTable sharedStringsTable; + // sheet的索引 + protected int sheetIndex; + + // 当前非空行 + protected int index; + // 当前列 + private int curCell; + // 单元数据类型 + private CellDataType cellDataType; + // 当前行号,从0开始 + private long rowNumber; + // 当前列坐标, 如A1,B5 + private String curCoordinate; + // 当前节点名称 + private ElementName curElementName; + // 前一个列的坐标 + private String preCoordinate; + // 行的最大列坐标 + private String maxCellCoordinate; + // 单元格样式 + private XSSFCellStyle xssfCellStyle; + // 单元格存储的格式化字符串,nmtFmt的formatCode属性的值 + private String numFmtString; + // 是否处于sheetData标签内,sax只解析此标签内的内容,其它标签忽略 + private boolean isInSheetData; + + // 上一次的内容 + private final StrBuilder lastContent = StrUtil.strBuilder(); + // 上一次的内容 + private final StrBuilder lastFormula = StrUtil.strBuilder(); + // 存储每行的列元素 + private List rowCellList = new ArrayList<>(); + + public SheetDataSaxHandler(RowHandler rowHandler){ + this.rowHandler = rowHandler; + } + + /** + * 行处理器 + */ + protected RowHandler rowHandler; + + /** + * 设置行处理器 + * + * @param rowHandler 行处理器 + */ + public void setRowHandler(RowHandler rowHandler) { + this.rowHandler = rowHandler; + } + + /** + * 读到一个xml开始标签时的回调处理方法 + */ + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) { + if ("sheetData".equals(qName)) { + this.isInSheetData = true; + return; + } + + if (false == this.isInSheetData) { + // 非sheetData标签,忽略解析 + return; + } + + final ElementName name = ElementName.of(qName); + this.curElementName = name; + + if (null != name) { + switch (name) { + case row: + // 行开始 + startRow(attributes); + break; + case c: + // 单元格元素 + startCell(attributes); + break; + } + } + } + + /** + * 标签结束的回调处理方法 + */ + @Override + public void endElement(String uri, String localName, String qName) { + if ("sheetData".equals(qName)) { + // sheetData结束,不再解析别的标签 + this.isInSheetData = false; + return; + } + + if (false == this.isInSheetData) { + // 非sheetData标签,忽略解析 + return; + } + + this.curElementName = null; + if (ElementName.c.match(qName)) { // 单元格结束 + endCell(); + } else if (ElementName.row.match(qName)) {// 行结束 + endRow(); + } + // 其它标签忽略 + } + + /** + * s标签结束的回调处理方法 + */ + @Override + public void characters(char[] ch, int start, int length) { + if (false == this.isInSheetData) { + // 非sheetData标签,忽略解析 + return; + } + + final ElementName elementName = this.curElementName; + if (null != elementName) { + switch (elementName) { + case v: + // 得到单元格内容的值 + lastContent.append(ch, start, length); + break; + case f: + // 得到单元格内容的值 + lastFormula.append(ch, start, length); + break; + } + } + // 其它标签忽略 + } + + // --------------------------------------------------------------------------------------- Private method start + + /** + * 行开始 + * + * @param attributes 属性列表 + */ + private void startRow(Attributes attributes) { + final String rValue = AttributeName.r.getValue(attributes); + if (null != rValue) { + this.rowNumber = Long.parseLong(rValue) - 1; + } + } + + /** + * 单元格开始 + * + * @param attributes 属性列表 + */ + private void startCell(Attributes attributes) { + // 获取当前列坐标 + final String tempCurCoordinate = AttributeName.r.getValue(attributes); + // 前一列为null,则将其设置为"@",A为第一列,ascii码为65,前一列即为@,ascii码64 + if (preCoordinate == null) { + preCoordinate = String.valueOf(ExcelSaxUtil.CELL_FILL_CHAR); + } else { + // 存在,则前一列要设置为上一列的坐标 + preCoordinate = curCoordinate; + } + // 重置当前列 + curCoordinate = tempCurCoordinate; + // 设置单元格类型 + setCellType(attributes); + + // 清空之前的数据 + lastContent.reset(); + lastFormula.reset(); + } + + /** + * 一行结尾 + */ + private void endRow() { + // 最大列坐标以第一个非空行的为准 + if (index == 0) { + maxCellCoordinate = curCoordinate; + } + + // 补全一行尾部可能缺失的单元格 + if (maxCellCoordinate != null) { + fillBlankCell(curCoordinate, maxCellCoordinate, true); + } + + rowHandler.handle(sheetIndex, rowNumber, rowCellList); + + // 一行结束 + // 新建一个新列,之前的列抛弃(可能被回收或rowHandler处理) + rowCellList = new ArrayList<>(curCell + 1); + // 行数增加 + index++; + // 当前列置0 + curCell = 0; + // 置空当前列坐标和前一列坐标 + curCoordinate = null; + preCoordinate = null; + } + + /** + * 一个单元格结尾 + */ + private void endCell() { + // 补全单元格之间的空格 + fillBlankCell(preCoordinate, curCoordinate, false); + + final String contentStr = StrUtil.trim(lastContent); + Object value = ExcelSaxUtil.getDataValue(this.cellDataType, contentStr, this.sharedStringsTable, this.numFmtString); + if (false == this.lastFormula.isEmpty()) { + value = new FormulaCellValue(StrUtil.trim(lastFormula), value); + } + addCellValue(curCell++, value); + } + + /** + * 在一行中的指定列增加值 + * + * @param index 位置 + * @param value 值 + */ + private void addCellValue(int index, Object value) { + this.rowCellList.add(index, value); + this.rowHandler.handleCell(this.sheetIndex, this.rowNumber, index, value, this.xssfCellStyle); + } + + /** + * 填充空白单元格,如果前一个单元格大于后一个,不需要填充
      + * + * @param preCoordinate 前一个单元格坐标 + * @param curCoordinate 当前单元格坐标 + * @param isEnd 是否为最后一个单元格 + */ + private void fillBlankCell(String preCoordinate, String curCoordinate, boolean isEnd) { + if (false == curCoordinate.equals(preCoordinate)) { + int len = ExcelSaxUtil.countNullCell(preCoordinate, curCoordinate); + if (isEnd) { + len++; + } + while (len-- > 0) { + addCellValue(curCell++, StrUtil.EMPTY); + } + } + } + + /** + * 设置单元格的类型 + * + * @param attributes 属性 + */ + private void setCellType(Attributes attributes) { + // numFmtString的值 + numFmtString = StrUtil.EMPTY; + this.cellDataType = CellDataType.of(AttributeName.t.getValue(attributes)); + + // 获取单元格的xf索引,对应style.xml中cellXfs的子元素xf + if (null != this.stylesTable) { + final String xfIndexStr = AttributeName.s.getValue(attributes); + if (null != xfIndexStr) { + this.xssfCellStyle = stylesTable.getStyleAt(Integer.parseInt(xfIndexStr)); + // 单元格存储格式的索引,对应style.xml中的numFmts元素的子元素索引 + final int numFmtIndex = xssfCellStyle.getDataFormat(); + this.numFmtString = ObjectUtil.defaultIfNull( + xssfCellStyle.getDataFormatString(), + BuiltinFormats.getBuiltinFormat(numFmtIndex)); + if (CellDataType.NUMBER == this.cellDataType && ExcelSaxUtil.isDateFormat(numFmtIndex, numFmtString)) { + cellDataType = CellDataType.DATE; + } + } + } + + } + + // --------------------------------------------------------------------------------------- Private method end +} From 73409012074d7387ad58d4041ff8fb77cc660582 Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 9 Dec 2020 00:43:00 +0800 Subject: [PATCH 29/47] fix code --- .../main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java index 4c55b90d2..c89a1bd33 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java @@ -120,11 +120,11 @@ public class Excel07SaxReader implements ExcelSaxReader { * @since 5.4.4 */ public Excel07SaxReader read(XSSFReader xssfReader, String idOrRid) throws POIException { - // 获取共享样式表 + // 获取共享样式表,样式非必须 try { this.handler.stylesTable = xssfReader.getStylesTable(); - } catch (Exception e) { - //ignore + } catch (IOException | InvalidFormatException ignore) { + // ignore } // 获取共享字符串表 From 5bb4ff9c40aa14cd417fc553994bcca9ef03f203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E5=BF=98=E5=88=9D=E5=BF=83?= Date: Wed, 9 Dec 2020 18:26:45 +0800 Subject: [PATCH 30/47] =?UTF-8?q?cn.hutool.core.collection.CollUtil#addAll?= =?UTF-8?q?(java.util.Collection,=20java.lang.Iterable)=20=E5=88=A4?= =?UTF-8?q?=E6=96=AD=E7=A9=BA=E6=8C=87=E9=92=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/cn/hutool/core/collection/CollUtil.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java index aa73d40f5..870594aa1 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java @@ -2108,6 +2108,9 @@ public class CollUtil { * @return 原集合 */ public static Collection addAll(Collection collection, Iterable iterable) { + if(iterable == null){ + return collection; + } return addAll(collection, iterable.iterator()); } From 0ee10efa041d9e0e5bf83acf85ae0a1ed914c50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E5=BF=98=E5=88=9D=E5=BF=83?= Date: Wed, 9 Dec 2020 18:27:01 +0800 Subject: [PATCH 31/47] =?UTF-8?q?cn.hutool.core.collection.CollUtil#addAll?= =?UTF-8?q?(java.util.Collection,=20java.lang.Iterable)=20=E5=88=A4?= =?UTF-8?q?=E6=96=AD=E7=A9=BA=E6=8C=87=E9=92=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/cn/hutool/core/collection/CollUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java index 870594aa1..7f629bc0f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java @@ -2108,7 +2108,7 @@ public class CollUtil { * @return 原集合 */ public static Collection addAll(Collection collection, Iterable iterable) { - if(iterable == null){ + if (iterable == null) { return collection; } return addAll(collection, iterable.iterator()); From 82496327ab1dd1f4ecec320c47580dbbd76131cd Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 10 Dec 2020 04:10:27 +0800 Subject: [PATCH 32/47] add null check --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0652a9506..de29d528b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------------------------------------------- -# 5.5.3 (2020-12-08) +# 5.5.3 (2020-12-10) ### 新特性 * 【core 】 IdcardUtil增加行政区划83(issue#1277@Github) @@ -16,6 +16,7 @@ * 【core 】 增加Segment(pr#1290@Github) * 【core 】 增加CharSequenceUtil * 【poi 】 Excel07SaxReader拆分出SheetDataSaxHandler +* 【core 】 CollUtil.addAll增加判空(pr#228@Gitee) ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) From b44dc1bb98d6af32d5ac122d4fd6c98684279350 Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 10 Dec 2020 23:53:21 +0800 Subject: [PATCH 33/47] fix bug --- CHANGELOG.md | 4 +++- .../main/java/cn/hutool/core/date/DateBetween.java | 4 ++-- .../src/main/java/cn/hutool/core/date/DateUtil.java | 4 ++-- .../src/main/java/cn/hutool/core/io/FileUtil.java | 11 ++--------- .../src/main/java/cn/hutool/core/util/IdcardUtil.java | 6 +++--- .../test/java/cn/hutool/core/util/IdcardUtilTest.java | 6 ++++++ 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de29d528b..3e902f422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------------------------------------------- -# 5.5.3 (2020-12-10) +# 5.5.3 (2020-12-11) ### 新特性 * 【core 】 IdcardUtil增加行政区划83(issue#1277@Github) @@ -17,6 +17,7 @@ * 【core 】 增加CharSequenceUtil * 【poi 】 Excel07SaxReader拆分出SheetDataSaxHandler * 【core 】 CollUtil.addAll增加判空(pr#228@Gitee) +* 【core 】 修正DateUtil.betweenXXX注释错误(issue#I28XGW@Gitee) ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) @@ -27,6 +28,7 @@ * 【cache 】 get中unlock改为unlockRead(issue#1294@Github) * 【db 】 修复表名包含点导致的问题(issue#1300@Github) * 【poi 】 修复xdr:row标签导致的问题(issue#1297@Github) +* 【core 】 修复FileUtil.loopFiles使用FileFilter无效问题(issue#I28V48@Gitee) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateBetween.java b/hutool-core/src/main/java/cn/hutool/core/date/DateBetween.java index f37b869be..4bfde24e9 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/DateBetween.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateBetween.java @@ -95,7 +95,7 @@ public class DateBetween implements Serializable{ /** * 计算两个日期相差月数
      - * 在非重置情况下,如果起始日期的天小于结束日期的天,月数要少算1(不足1个月) + * 在非重置情况下,如果起始日期的天大于结束日期的天,月数要少算1(不足1个月) * * @param isReset 是否重置时间为起始时间(重置天时分秒) * @return 相差月数 @@ -122,7 +122,7 @@ public class DateBetween implements Serializable{ /** * 计算两个日期相差年数
      - * 在非重置情况下,如果起始日期的月小于结束日期的月,年数要少算1(不足1年) + * 在非重置情况下,如果起始日期的月大于结束日期的月,年数要少算1(不足1年) * * @param isReset 是否重置时间为起始时间(重置月天时分秒) * @return 相差年数 diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java index 10902ac94..c08f46b7d 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java @@ -1375,7 +1375,7 @@ public class DateUtil extends CalendarUtil { /** * 计算两个日期相差月数
      - * 在非重置情况下,如果起始日期的天小于结束日期的天,月数要少算1(不足1个月) + * 在非重置情况下,如果起始日期的天大于结束日期的天,月数要少算1(不足1个月) * * @param beginDate 起始日期 * @param endDate 结束日期 @@ -1389,7 +1389,7 @@ public class DateUtil extends CalendarUtil { /** * 计算两个日期相差年数
      - * 在非重置情况下,如果起始日期的月小于结束日期的月,年数要少算1(不足1年) + * 在非重置情况下,如果起始日期的月大于结束日期的月,年数要少算1(不足1年) * * @param beginDate 起始日期 * @param endDate 结束日期 diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java index 039845a71..2eccd286e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java @@ -1,7 +1,6 @@ package cn.hutool.core.io; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.collection.ListUtil; import cn.hutool.core.io.file.FileCopier; import cn.hutool.core.io.file.FileMode; import cn.hutool.core.io.file.FileNameUtil; @@ -173,13 +172,7 @@ public class FileUtil extends PathUtil { * @return 文件列表 */ public static List loopFiles(File file, FileFilter fileFilter) { - if (null == file || false == file.exists()) { - return ListUtil.empty(); - } - - final List fileList = new ArrayList<>(); - walkFiles(file, fileList::add); - return fileList; + return loopFiles(file, -1, fileFilter); } /** @@ -216,7 +209,7 @@ public class FileUtil extends PathUtil { * @return 文件列表 * @since 4.6.3 */ - public static List loopFiles(File file, int maxDepth, final FileFilter fileFilter) { + public static List loopFiles(File file, int maxDepth, FileFilter fileFilter) { return loopFiles(file.toPath(), maxDepth, fileFilter); } diff --git a/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java index 4502ba103..af9dc87de 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java @@ -510,7 +510,7 @@ public class IdcardUtil { } /** - * 根据身份编号获取户籍省份,只支持15或18位身份证号码 + * 根据身份编号获取市级编码,只支持15或18位身份证号码 * * @param idcard 身份编码 * @return 市级编码。 @@ -658,9 +658,9 @@ public class IdcardUtil { } /** - * 获取省份代码 + * 获取市级编码 * - * @return 省份代码 + * @return 市级编码 */ public String getCityCode() { return this.cityCode; diff --git a/hutool-core/src/test/java/cn/hutool/core/util/IdcardUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/IdcardUtilTest.java index 800c10070..c25a9fdf8 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/IdcardUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/IdcardUtilTest.java @@ -75,6 +75,12 @@ public class IdcardUtilTest { Assert.assertEquals(province2, "内蒙古"); } + @Test + public void getCityCodeByIdCardTest() { + String codeByIdCard = IdcardUtil.getCityCodeByIdCard(ID_18); + Assert.assertEquals("32108", codeByIdCard); + } + @Test public void getGenderByIdCardTest() { int gender = IdcardUtil.getGenderByIdCard(ID_18); From 4f6155c45ac7fa36a34d5ee3aa477f1a0e4f7d67 Mon Sep 17 00:00:00 2001 From: Looly Date: Fri, 11 Dec 2020 01:47:51 +0800 Subject: [PATCH 34/47] add NioUtil --- CHANGELOG.md | 2 + .../main/java/cn/hutool/core/io/IoUtil.java | 254 ++++-------------- .../main/java/cn/hutool/core/io/NioUtil.java | 227 ++++++++++++++++ .../java/cn/hutool/extra/ssh/JschUtil.java | 8 +- 4 files changed, 285 insertions(+), 206 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/NioUtil.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e902f422..8deccb205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * 【poi 】 Excel07SaxReader拆分出SheetDataSaxHandler * 【core 】 CollUtil.addAll增加判空(pr#228@Gitee) * 【core 】 修正DateUtil.betweenXXX注释错误(issue#I28XGW@Gitee) +* 【core 】 增加NioUtil ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) @@ -29,6 +30,7 @@ * 【db 】 修复表名包含点导致的问题(issue#1300@Github) * 【poi 】 修复xdr:row标签导致的问题(issue#1297@Github) * 【core 】 修复FileUtil.loopFiles使用FileFilter无效问题(issue#I28V48@Gitee) +* 【extra 】 修复JschUtil.execByShell返回空的问题(issue#1067@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java index 82c941f72..6feba0a8f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java @@ -28,13 +28,8 @@ import java.io.PushbackReader; import java.io.Reader; import java.io.Serializable; import java.io.Writer; -import java.nio.ByteBuffer; import java.nio.CharBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.Channels; import java.nio.channels.FileChannel; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.util.Collection; import java.util.Objects; @@ -48,25 +43,7 @@ import java.util.zip.Checksum; * * @author xiaoleilu */ -public class IoUtil { - - /** - * 默认缓存大小 8192 - */ - public static final int DEFAULT_BUFFER_SIZE = 2 << 12; - /** - * 默认中等缓存大小 16384 - */ - public static final int DEFAULT_MIDDLE_BUFFER_SIZE = 2 << 13; - /** - * 默认大缓存大小 32768 - */ - public static final int DEFAULT_LARGE_BUFFER_SIZE = 2 << 14; - - /** - * 数据流末尾 - */ - public static final int EOF = -1; +public class IoUtil extends NioUtil{ // -------------------------------------------------------------------------------------- Copy start @@ -195,21 +172,6 @@ public class IoUtil { return size; } - /** - * 拷贝流 thanks to: https://github.com/venusdrogon/feilong-io/blob/master/src/main/java/com/feilong/io/IOWriteUtil.java
      - * 本方法不会关闭流 - * - * @param in 输入流 - * @param out 输出流 - * @param bufferSize 缓存大小 - * @param streamProgress 进度条 - * @return 传输的byte数 - * @throws IORuntimeException IO异常 - */ - public static long copyByNIO(InputStream in, OutputStream out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException { - return copy(Channels.newChannel(in), Channels.newChannel(out), bufferSize, streamProgress); - } - /** * 拷贝文件流,使用NIO * @@ -227,79 +189,13 @@ public class IoUtil { try { inChannel = in.getChannel(); outChannel = out.getChannel(); - return inChannel.transferTo(0, inChannel.size(), outChannel); - } catch (IOException e) { - throw new IORuntimeException(e); + return copy(inChannel, outChannel); } finally { close(outChannel); close(inChannel); } } - /** - * 拷贝流,使用NIO,不会关闭流 - * - * @param in {@link ReadableByteChannel} - * @param out {@link WritableByteChannel} - * @return 拷贝的字节数 - * @throws IORuntimeException IO异常 - * @since 4.5.0 - */ - public static long copy(ReadableByteChannel in, WritableByteChannel out) throws IORuntimeException { - return copy(in, out, DEFAULT_BUFFER_SIZE); - } - - /** - * 拷贝流,使用NIO,不会关闭流 - * - * @param in {@link ReadableByteChannel} - * @param out {@link WritableByteChannel} - * @param bufferSize 缓冲大小,如果小于等于0,使用默认 - * @return 拷贝的字节数 - * @throws IORuntimeException IO异常 - * @since 4.5.0 - */ - public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize) throws IORuntimeException { - return copy(in, out, bufferSize, null); - } - - /** - * 拷贝流,使用NIO,不会关闭流 - * - * @param in {@link ReadableByteChannel} - * @param out {@link WritableByteChannel} - * @param bufferSize 缓冲大小,如果小于等于0,使用默认 - * @param streamProgress {@link StreamProgress}进度处理器 - * @return 拷贝的字节数 - * @throws IORuntimeException IO异常 - */ - public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException { - Assert.notNull(in, "InputStream is null !"); - Assert.notNull(out, "OutputStream is null !"); - - ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize <= 0 ? DEFAULT_BUFFER_SIZE : bufferSize); - long size = 0; - if (null != streamProgress) { - streamProgress.start(); - } - try { - while (in.read(byteBuffer) != EOF) { - byteBuffer.flip();// 写转读 - size += out.write(byteBuffer); - byteBuffer.clear(); - if (null != streamProgress) { - streamProgress.progress(size); - } - } - } catch (IOException e) { - throw new IORuntimeException(e); - } - if (null != streamProgress) { - streamProgress.finish(); - } - - return size; - } // -------------------------------------------------------------------------------------- Copy end // -------------------------------------------------------------------------------------- getReader and getWriter start @@ -455,22 +351,7 @@ public class IoUtil { * @throws IORuntimeException IO异常 */ public static String read(InputStream in, Charset charset) throws IORuntimeException { - FastByteArrayOutputStream out = read(in); - return null == charset ? out.toString() : out.toString(charset); - } - - /** - * 从流中读取内容,读取完毕后并不关闭流 - * - * @param channel 可读通道,读取完毕后并不关闭通道 - * @param charset 字符集 - * @return 内容 - * @throws IORuntimeException IO异常 - * @since 4.5.0 - */ - public static String read(ReadableByteChannel channel, Charset charset) throws IORuntimeException { - FastByteArrayOutputStream out = read(channel); - return null == charset ? out.toString() : out.toString(charset); + return StrUtil.str(readBytes(in), charset); } /** @@ -486,19 +367,6 @@ public class IoUtil { return out; } - /** - * 从流中读取内容,读到输出流中 - * - * @param channel 可读通道,读取完毕后并不关闭通道 - * @return 输出流 - * @throws IORuntimeException IO异常 - */ - public static FastByteArrayOutputStream read(ReadableByteChannel channel) throws IORuntimeException { - final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); - copy(channel, Channels.newChannel(out)); - return out; - } - /** * 从Reader中读取String,读取完毕后关闭Reader * @@ -513,7 +381,7 @@ public class IoUtil { /** * 从{@link Reader}中读取String * - * @param reader {@link Reader} + * @param reader {@link Reader} * @param isClose 是否关闭{@link Reader} * @return String * @throws IORuntimeException IO异常 @@ -527,55 +395,14 @@ public class IoUtil { } } catch (IOException e) { throw new IORuntimeException(e); - } finally{ - if(isClose){ + } finally { + if (isClose) { IoUtil.close(reader); } } return builder.toString(); } - /** - * 从FileChannel中读取UTF-8编码内容 - * - * @param fileChannel 文件管道 - * @return 内容 - * @throws IORuntimeException IO异常 - */ - public static String readUtf8(FileChannel fileChannel) throws IORuntimeException { - return read(fileChannel, CharsetUtil.CHARSET_UTF_8); - } - - /** - * 从FileChannel中读取内容,读取完毕后并不关闭Channel - * - * @param fileChannel 文件管道 - * @param charsetName 字符集 - * @return 内容 - * @throws IORuntimeException IO异常 - */ - public static String read(FileChannel fileChannel, String charsetName) throws IORuntimeException { - return read(fileChannel, CharsetUtil.charset(charsetName)); - } - - /** - * 从FileChannel中读取内容 - * - * @param fileChannel 文件管道 - * @param charset 字符集 - * @return 内容 - * @throws IORuntimeException IO异常 - */ - public static String read(FileChannel fileChannel, Charset charset) throws IORuntimeException { - MappedByteBuffer buffer; - try { - buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()).load(); - } catch (IOException e) { - throw new IORuntimeException(e); - } - return StrUtil.str(buffer, charset); - } - /** * 从流中读取bytes,读取完毕后关闭流 * @@ -597,12 +424,19 @@ public class IoUtil { * @since 5.0.4 */ public static byte[] readBytes(InputStream in, boolean isCloseStream) throws IORuntimeException { - final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); - copy(in, out); - if (isCloseStream) { - close(in); + final InputStream availableStream = toAvailableStream(in); + try{ + final int available = availableStream.available(); + if(available > 0){ + byte[] result = new byte[available]; + //noinspection ResultOfMethodCallIgnored + availableStream.read(result); + return result; + } + } catch (IOException e){ + throw new IORuntimeException(e); } - return out.toByteArray(); + return new byte[0]; } /** @@ -966,6 +800,42 @@ public class IoUtil { return (in instanceof PushbackInputStream) ? (PushbackInputStream) in : new PushbackInputStream(in, pushBackSize); } + /** + * 将指定{@link InputStream} 转换为{@link InputStream#available()}方法可用的流。
      + * 在Socket通信流中,服务端未返回数据情况下{@link InputStream#available()}方法始终为{@code 0}
      + * 因此,在读取前需要调用{@link InputStream#read()}读取一个字节(未返回会阻塞),一旦读取到了,{@link InputStream#available()}方法就正常了。
      + * 此方法返回对象的规则为: + * + *
        + *
      • FileInputStream 返回原对象,因为文件流的available方法本身可用
      • + *
      • 其它InputStream 返回PushbackInputStream
      • + *
      + * + * @param in 被转换的流 + * @return 转换后的流,可能为{@link PushbackInputStream} + * @since 5.5.3 + */ + public static InputStream toAvailableStream(InputStream in) { + if(in instanceof FileInputStream){ + // FileInputStream本身支持available方法。 + return in; + } + + final PushbackInputStream pushbackInputStream = toPushbackStream(in, 1); + try { + final int available = pushbackInputStream.available(); + if (available <= 0) { + //此操作会阻塞,直到有数据被读到 + int b = pushbackInputStream.read(); + pushbackInputStream.unread(b); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + + return pushbackInputStream; + } + /** * 将byte[]写到流中 * @@ -1113,22 +983,6 @@ public class IoUtil { } } - /** - * 关闭
      - * 关闭失败不会抛出异常 - * - * @param closeable 被关闭的对象 - */ - public static void close(AutoCloseable closeable) { - if (null != closeable) { - try { - closeable.close(); - } catch (Exception e) { - // 静默关闭 - } - } - } - /** * 尝试关闭指定对象
      * 判断对象如果实现了{@link AutoCloseable},则调用之 diff --git a/hutool-core/src/main/java/cn/hutool/core/io/NioUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/NioUtil.java new file mode 100644 index 000000000..bed52ac53 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/NioUtil.java @@ -0,0 +1,227 @@ +package cn.hutool.core.io; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; + +/** + * NIO相关工具封装,主要针对Channel读写、拷贝等封装 + * + * @author looly + * @since 5.5.3 + */ +public class NioUtil { + + /** + * 默认缓存大小 8192 + */ + public static final int DEFAULT_BUFFER_SIZE = 2 << 12; + /** + * 默认中等缓存大小 16384 + */ + public static final int DEFAULT_MIDDLE_BUFFER_SIZE = 2 << 13; + /** + * 默认大缓存大小 32768 + */ + public static final int DEFAULT_LARGE_BUFFER_SIZE = 2 << 14; + + /** + * 数据流末尾 + */ + public static final int EOF = -1; + + /** + * 拷贝流 thanks to: https://github.com/venusdrogon/feilong-io/blob/master/src/main/java/com/feilong/io/IOWriteUtil.java
      + * 本方法不会关闭流 + * + * @param in 输入流 + * @param out 输出流 + * @param bufferSize 缓存大小 + * @param streamProgress 进度条 + * @return 传输的byte数 + * @throws IORuntimeException IO异常 + */ + public static long copyByNIO(InputStream in, OutputStream out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException { + return copy(Channels.newChannel(in), Channels.newChannel(out), bufferSize, streamProgress); + } + + /** + * 拷贝文件Channel,使用NIO,拷贝后不会关闭channel + * + * @param inChannel {@link FileChannel} + * @param outChannel {@link FileChannel} + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + * @since 5.5.3 + */ + public static long copy(FileChannel inChannel, FileChannel outChannel) throws IORuntimeException { + Assert.notNull(inChannel, "In channel is null!"); + Assert.notNull(outChannel, "Out channel is null!"); + + try { + return inChannel.transferTo(0, inChannel.size(), outChannel); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 拷贝流,使用NIO,不会关闭channel + * + * @param in {@link ReadableByteChannel} + * @param out {@link WritableByteChannel} + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + * @since 4.5.0 + */ + public static long copy(ReadableByteChannel in, WritableByteChannel out) throws IORuntimeException { + return copy(in, out, DEFAULT_BUFFER_SIZE); + } + + /** + * 拷贝流,使用NIO,不会关闭channel + * + * @param in {@link ReadableByteChannel} + * @param out {@link WritableByteChannel} + * @param bufferSize 缓冲大小,如果小于等于0,使用默认 + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + * @since 4.5.0 + */ + public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize) throws IORuntimeException { + return copy(in, out, bufferSize, null); + } + + /** + * 拷贝流,使用NIO,不会关闭channel + * + * @param in {@link ReadableByteChannel} + * @param out {@link WritableByteChannel} + * @param bufferSize 缓冲大小,如果小于等于0,使用默认 + * @param streamProgress {@link StreamProgress}进度处理器 + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + */ + public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize, StreamProgress streamProgress) throws IORuntimeException { + Assert.notNull(in, "InputStream is null !"); + Assert.notNull(out, "OutputStream is null !"); + + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize <= 0 ? DEFAULT_BUFFER_SIZE : bufferSize); + long size = 0; + if (null != streamProgress) { + streamProgress.start(); + } + try { + while (in.read(byteBuffer) != EOF) { + byteBuffer.flip();// 写转读 + size += out.write(byteBuffer); + byteBuffer.clear(); + if (null != streamProgress) { + streamProgress.progress(size); + } + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + if (null != streamProgress) { + streamProgress.finish(); + } + + return size; + } + + /** + * 从流中读取内容,读取完毕后并不关闭流 + * + * @param channel 可读通道,读取完毕后并不关闭通道 + * @param charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + * @since 4.5.0 + */ + public static String read(ReadableByteChannel channel, Charset charset) throws IORuntimeException { + FastByteArrayOutputStream out = read(channel); + return null == charset ? out.toString() : out.toString(charset); + } + + /** + * 从流中读取内容,读到输出流中 + * + * @param channel 可读通道,读取完毕后并不关闭通道 + * @return 输出流 + * @throws IORuntimeException IO异常 + */ + public static FastByteArrayOutputStream read(ReadableByteChannel channel) throws IORuntimeException { + final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + copy(channel, Channels.newChannel(out)); + return out; + } + + /** + * 从FileChannel中读取UTF-8编码内容 + * + * @param fileChannel 文件管道 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String readUtf8(FileChannel fileChannel) throws IORuntimeException { + return read(fileChannel, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 从FileChannel中读取内容,读取完毕后并不关闭Channel + * + * @param fileChannel 文件管道 + * @param charsetName 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String read(FileChannel fileChannel, String charsetName) throws IORuntimeException { + return read(fileChannel, CharsetUtil.charset(charsetName)); + } + + /** + * 从FileChannel中读取内容 + * + * @param fileChannel 文件管道 + * @param charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String read(FileChannel fileChannel, Charset charset) throws IORuntimeException { + MappedByteBuffer buffer; + try { + buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()).load(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return StrUtil.str(buffer, charset); + } + + /** + * 关闭
      + * 关闭失败不会抛出异常 + * + * @param closeable 被关闭的对象 + */ + public static void close(AutoCloseable closeable) { + if (null != closeable) { + try { + closeable.close(); + } catch (Exception e) { + // 静默关闭 + } + } + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java index 199c39397..8d2df2a45 100644 --- a/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java @@ -432,7 +432,7 @@ public class JschUtil { try { channel.connect(); in = channel.getInputStream(); - return IoUtil.read(in, CharsetUtil.CHARSET_UTF_8); + return IoUtil.read(in, charset); } catch (IOException e) { throw new IORuntimeException(e); } catch (JSchException e) { @@ -461,7 +461,6 @@ public class JschUtil { shell.setPty(true); OutputStream out = null; InputStream in = null; - final StringBuilder result = StrUtil.builder(); try { out = shell.getOutputStream(); in = shell.getInputStream(); @@ -469,9 +468,7 @@ public class JschUtil { out.write(StrUtil.bytes(cmd, charset)); out.flush(); - while (in.available() > 0) { - result.append(IoUtil.read(in, charset)); - } + return IoUtil.read(in, charset); } catch (IOException e) { throw new IORuntimeException(e); } finally { @@ -479,7 +476,6 @@ public class JschUtil { IoUtil.close(in); close(shell); } - return result.toString(); } /** From 726b55a742c886ae8edc6c3e73ba7615bb34979e Mon Sep 17 00:00:00 2001 From: Looly Date: Fri, 11 Dec 2020 02:45:22 +0800 Subject: [PATCH 35/47] add GanymedUtil --- CHANGELOG.md | 1 + hutool-extra/pom.xml | 7 + .../java/cn/hutool/extra/ssh/GanymedUtil.java | 130 ++++++++++++++++++ .../java/cn/hutool/extra/ssh/JschUtil.java | 2 +- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ssh/GanymedUtil.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8deccb205..381188e2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * 【core 】 CollUtil.addAll增加判空(pr#228@Gitee) * 【core 】 修正DateUtil.betweenXXX注释错误(issue#I28XGW@Gitee) * 【core 】 增加NioUtil +* 【core 】 增加GanymedUtil ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml index 524fe3c25..646a16e3f 100644 --- a/hutool-extra/pom.xml +++ b/hutool-extra/pom.xml @@ -130,6 +130,13 @@ compile true + + ch.ethz.ganymed + ganymed-ssh2 + 262 + compile + true + com.google.zxing diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/GanymedUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/GanymedUtil.java new file mode 100644 index 000000000..33ea88ed1 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/GanymedUtil.java @@ -0,0 +1,130 @@ +package cn.hutool.extra.ssh; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.Session; +import ch.ethz.ssh2.StreamGobbler; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * Ganymed-SSH2封装,见:http://www.ganymed.ethz.ch/ssh2/ + * + * @author looly + * @since 5.5.3 + */ +public class GanymedUtil { + + /** + * 连接到服务器 + * + * @param sshHost 主机 + * @param sshPort 端口 + * @return {@link Connection} + */ + public static Connection connect(String sshHost, int sshPort) { + Connection conn = new Connection(sshHost, sshPort); + try { + conn.connect(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return conn; + } + + /** + * 打开远程会话 + * + * @param sshHost 主机 + * @param sshPort 端口 + * @param sshUser 用户名,如果为null,默认root + * @param sshPass 密码 + * @return {@link Session} + */ + public static Session openSession(String sshHost, int sshPort, String sshUser, String sshPass) { + // 默认root用户 + if (StrUtil.isEmpty(sshUser)) { + sshUser = "root"; + } + + final Connection connect = connect(sshHost, sshPort); + try { + connect.authenticateWithPassword(sshUser, sshPass); + return connect.openSession(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 执行Shell命令(使用EXEC方式) + *

      + * 此方法单次发送一个命令到服务端,不读取环境变量,执行结束后自动关闭Session,不会产生阻塞。 + *

      + * + * @param session Session会话 + * @param cmd 命令 + * @param charset 发送和读取内容的编码 + * @param errStream 错误信息输出到的位置 + */ + public static String exec(Session session, String cmd, Charset charset, OutputStream errStream) { + final String result; + try { + session.execCommand(cmd, charset.name()); + result = IoUtil.read(new StreamGobbler(session.getStdout()), charset); + + // 错误输出 + IoUtil.copy(new StreamGobbler(session.getStdout()), errStream); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + close(session); + } + return result; + } + + /** + * 执行Shell命令 + *

      + * 此方法单次发送一个命令到服务端,自动读取环境变量,执行结束后自动关闭Session,不会产生阻塞。 + *

      + * + * @param session Session会话 + * @param cmd 命令 + * @param charset 发送和读取内容的编码 + * @param errStream 错误信息输出到的位置 + */ + public static String execByShell(Session session, String cmd, Charset charset, OutputStream errStream) { + final String result; + try { + session.requestDumbPTY(); + IoUtil.write(session.getStdin(), charset, true, cmd); + + result = IoUtil.read(new StreamGobbler(session.getStdout()), charset); + if(null != errStream){ + // 错误输出 + IoUtil.copy(new StreamGobbler(session.getStdout()), errStream); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + close(session); + } + return result; + } + + /** + * 关闭会话 + * + * @param session 会话通道 + */ + public static void close(Session session) { + if (session != null) { + session.close(); + } + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java index 8d2df2a45..9222dc315 100644 --- a/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java @@ -417,7 +417,7 @@ public class JschUtil { * @param cmd 命令 * @param charset 发送和读取内容的编码 * @param errStream 错误信息输出到的位置 - * @return {@link ChannelExec} + * @return 执行结果内容 * @since 4.3.1 */ public static String exec(Session session, String cmd, Charset charset, OutputStream errStream) { From 3033114e06b242dbdb813d1caf565244232fe749 Mon Sep 17 00:00:00 2001 From: Looly Date: Sat, 12 Dec 2020 02:09:16 +0800 Subject: [PATCH 36/47] fix excel sax bug --- CHANGELOG.md | 3 +- .../java/cn/hutool/core/bean/BeanUtil.java | 14 +++++----- .../java/cn/hutool/core/util/ReflectUtil.java | 28 +++++++++---------- .../poi/excel/sax/SheetDataSaxHandler.java | 8 +++--- .../cn/hutool/poi/excel/ExcelSaxReadTest.java | 11 +++----- 5 files changed, 31 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 381188e2a..0483e04d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ * 【poi 】 修复xdr:row标签导致的问题(issue#1297@Github) * 【core 】 修复FileUtil.loopFiles使用FileFilter无效问题(issue#I28V48@Gitee) * 【extra 】 修复JschUtil.execByShell返回空的问题(issue#1067@Github) +* 【poi 】 修复特殊的excel使用sax读取时未读到值的问题(issue#1303@Github) ------------------------------------------------------------------------------------------------------------- @@ -72,7 +73,7 @@ * 【core 】 修复HexUtil.format问题(issue#I268XT@Gitee) * 【core 】 修复ZipUtil判断压缩文件是否位于压缩目录内的逻辑有误的问题(issue#1251@Github) * 【json 】 修复JSONObject.accumulate问题 -* 【core 】 修复部分xlsx文件sax方式解析空指针问题(issue#1265@Github) +* 【poi 】 修复部分xlsx文件sax方式解析空指针问题(issue#1265@Github) * 【core 】 修复PatternPool中邮编的正则(issue#1274@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java index a87de2f4f..33fafbd4d 100644 --- a/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java @@ -745,11 +745,11 @@ public class BeanUtil { } /** - * 判断Bean是否为非空对象,非空对象表示本身不为null或者含有非null属性的对象 + * 判断Bean是否为非空对象,非空对象表示本身不为{@code null}或者含有非{@code null}属性的对象 * * @param bean Bean对象 * @param ignoreFiledNames 忽略检查的字段名 - * @return 是否为空,true - 空 / false - 非空 + * @return 是否为空,{@code true} - 空 / {@code false} - 非空 * @since 5.0.7 */ public static boolean isNotEmpty(Object bean, String... ignoreFiledNames) { @@ -757,12 +757,12 @@ public class BeanUtil { } /** - * 判断Bean是否为空对象,空对象表示本身为null或者所有属性都为null
      + * 判断Bean是否为空对象,空对象表示本身为{@code null}或者所有属性都为{@code null}
      * 此方法不判断static属性 * * @param bean Bean对象 * @param ignoreFiledNames 忽略检查的字段名 - * @return 是否为空,true - 空 / false - 非空 + * @return 是否为空,{@code true} - 空 / {@code false} - 非空 * @since 4.1.10 */ public static boolean isEmpty(Object bean, String... ignoreFiledNames) { @@ -781,12 +781,12 @@ public class BeanUtil { } /** - * 判断Bean是否包含值为null的属性
      - * 对象本身为null也返回true + * 判断Bean是否包含值为{@code null}的属性
      + * 对象本身为{@code null}也返回true * * @param bean Bean对象 * @param ignoreFiledNames 忽略检查的字段名 - * @return 是否包含值为null的属性,true - 包含 / false - 不包含 + * @return 是否包含值为null的属性,{@code true} - 包含 / {@code false} - 不包含 * @since 4.1.10 */ public static boolean hasNullField(Object bean, String... ignoreFiledNames) { diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java index 48f116ea1..ec6606321 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java @@ -140,7 +140,7 @@ public class ReflectUtil { } /** - * 查找指定类中的指定name的字段(包括非public字段),也包括父类和Object类的字段, 字段不存在则返回null + * 查找指定类中的指定name的字段(包括非public字段),也包括父类和Object类的字段, 字段不存在则返回{@code null} * * @param beanClass 被查找字段的类,不能为null * @param name 字段名 @@ -422,7 +422,7 @@ public class ReflectUtil { } /** - * 查找指定Public方法 如果找不到对应的方法或方法不为public的则返回null + * 查找指定Public方法 如果找不到对应的方法或方法不为public的则返回{@code null} * * @param clazz 类 * @param methodName 方法名 @@ -442,7 +442,7 @@ public class ReflectUtil { * 查找指定对象中的所有方法(包括非public方法),也包括父对象和Object类的方法 * *

      - * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回null。 + * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回{@code null}。 *

      * * @param obj 被查找的对象,如果为{@code null}返回{@code null} @@ -459,10 +459,10 @@ public class ReflectUtil { } /** - * 忽略大小写查找指定方法,如果找不到对应的方法则返回null + * 忽略大小写查找指定方法,如果找不到对应的方法则返回{@code null} * *

      - * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回null。 + * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回{@code null}。 *

      * * @param clazz 类,如果为{@code null}返回{@code null} @@ -477,10 +477,10 @@ public class ReflectUtil { } /** - * 查找指定方法 如果找不到对应的方法则返回null + * 查找指定方法 如果找不到对应的方法则返回{@code null} * *

      - * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回null。 + * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回{@code null}。 *

      * * @param clazz 类,如果为{@code null}返回{@code null} @@ -494,10 +494,10 @@ public class ReflectUtil { } /** - * 查找指定方法 如果找不到对应的方法则返回null + * 查找指定方法 如果找不到对应的方法则返回{@code null} * *

      - * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回null。 + * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回{@code null}。 *

      * * @param clazz 类,如果为{@code null}返回{@code null} @@ -527,7 +527,7 @@ public class ReflectUtil { } /** - * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回null + * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回{@code null} * *

      * 此方法只检查方法名是否一致,并不检查参数的一致性。 @@ -544,7 +544,7 @@ public class ReflectUtil { } /** - * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回null + * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回{@code null} * *

      * 此方法只检查方法名是否一致(忽略大小写),并不检查参数的一致性。 @@ -561,7 +561,7 @@ public class ReflectUtil { } /** - * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回null + * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回{@code null} * *

      * 此方法只检查方法名是否一致,并不检查参数的一致性。 @@ -842,7 +842,7 @@ public class ReflectUtil { * * * @param 返回对象类型 - * @param obj 对象,如果执行静态方法,此值为null + * @param obj 对象,如果执行静态方法,此值为{@code null} * @param method 方法(对象方法或static方法都可) * @param args 参数对象 * @return 结果 @@ -878,7 +878,7 @@ public class ReflectUtil { * * * @param 返回对象类型 - * @param obj 对象,如果执行静态方法,此值为null + * @param obj 对象,如果执行静态方法,此值为{@code null} * @param method 方法(对象方法或static方法都可) * @param args 参数对象 * @return 结果 diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetDataSaxHandler.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetDataSaxHandler.java index b6d63ee7b..c703691e9 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetDataSaxHandler.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/SheetDataSaxHandler.java @@ -137,9 +137,6 @@ public class SheetDataSaxHandler extends DefaultHandler { // 其它标签忽略 } - /** - * s标签结束的回调处理方法 - */ @Override public void characters(char[] ch, int start, int length) { if (false == this.isInSheetData) { @@ -159,8 +156,11 @@ public class SheetDataSaxHandler extends DefaultHandler { lastFormula.append(ch, start, length); break; } + } else{ + // 按理说内容应该为"内容",但是某些特别的XML内容不在v或f标签中,此处做一些兼容 + // issue#1303@Github + lastContent.append(ch, start, length); } - // 其它标签忽略 } // --------------------------------------------------------------------------------------- Private method start diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java index 4b07c60af..4d5ef39f8 100644 --- a/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/ExcelSaxReadTest.java @@ -64,7 +64,7 @@ public class ExcelSaxReadTest { @Test @Ignore public void readBySaxTest2() { - ExcelUtil.readBySax("d:/test/default.xlsx", -1, (sheetIndex, rowIndex, rowList) -> Console.log(rowList)); + ExcelUtil.readBySax("d:/test/456789.xlsx", "0", (sheetIndex, rowIndex, rowList) -> Console.log(rowList)); } private RowHandler createRowHandler() { @@ -140,9 +140,7 @@ public class ExcelSaxReadTest { public void dateReadXlsTest() { List rows = new ArrayList<>(); ExcelUtil.readBySax("data_for_sax_test.xls", 0, - (i, i1, list) ->{ - rows.add(StrUtil.toString(list.get(0))); - } + (i, i1, list) -> rows.add(StrUtil.toString(list.get(0))) ); Assert.assertEquals("2020-10-09 00:00:00", rows.get(1)); @@ -179,8 +177,7 @@ public class ExcelSaxReadTest { @Test @Ignore public void readXlsmTest(){ - ExcelUtil.readBySax("d:/test/WhiteListTemplate.xlsm", -1, (sheetIndex, rowIndex, rowlist) -> { - Console.log("[{}] [{}] {}", sheetIndex, rowIndex, rowlist); - }); + ExcelUtil.readBySax("d:/test/WhiteListTemplate.xlsm", -1, + (sheetIndex, rowIndex, rowlist) -> Console.log("[{}] [{}] {}", sheetIndex, rowIndex, rowlist)); } } From b91561efe68ccfc6c3076509d772ae922803770a Mon Sep 17 00:00:00 2001 From: Looly Date: Sat, 12 Dec 2020 03:08:11 +0800 Subject: [PATCH 37/47] add ofd --- CHANGELOG.md | 1 + hutool-poi/pom.xml | 7 ++-- .../java/cn/hutool/poi/ofd/OfdWriter.java | 42 +++++++++++++++++++ .../java/cn/hutool/poi/ofd/package-info.java | 6 +++ 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/ofd/package-info.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0483e04d4..8073a66c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * 【core 】 修正DateUtil.betweenXXX注释错误(issue#I28XGW@Gitee) * 【core 】 增加NioUtil * 【core 】 增加GanymedUtil +* 【poi 】 增加OFD支持,OfdWriter ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-poi/pom.xml b/hutool-poi/pom.xml index 8e67a85d7..d82e04e0e 100644 --- a/hutool-poi/pom.xml +++ b/hutool-poi/pom.xml @@ -18,7 +18,6 @@ 4.1.2 - 2.12.0 @@ -42,9 +41,9 @@ true - xerces - xercesImpl - ${xerces.version} + org.ofdrw + ofdrw-full + 1.7.2 compile true diff --git a/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java b/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java new file mode 100644 index 000000000..b0271c490 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java @@ -0,0 +1,42 @@ +package cn.hutool.poi.ofd; + +import cn.hutool.core.io.IoUtil; +import org.ofdrw.font.Font; +import org.ofdrw.layout.OFDDoc; +import org.ofdrw.layout.element.Div; +import org.ofdrw.layout.element.Paragraph; + +import java.io.Closeable; +import java.io.Serializable; +import java.nio.file.Path; + +public class OfdWriter implements Serializable, Closeable { + private static final long serialVersionUID = 1L; + + private final Path destFile; + private final OFDDoc doc; + + public OfdWriter(Path file){ + this.destFile = file; + this.doc = new OFDDoc(file); + } + + public OfdWriter addText(Font font, String... texts){ + final Paragraph paragraph = new Paragraph(); + paragraph.setDefaultFont(font); + for (String text : texts) { + paragraph.add(text); + } + return add(paragraph); + } + + public OfdWriter add(Div div){ + this.doc.add(div); + return this; + } + + @Override + public void close() { + IoUtil.close(this.doc); + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/ofd/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/ofd/package-info.java new file mode 100644 index 000000000..e6e362122 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/ofd/package-info.java @@ -0,0 +1,6 @@ +/** + * 开放版式文档(Open Fixed-layout Document )封装,基于ofdrw(https://gitee.com/Trisia/ofdrw) + * + * @author looly + */ +package cn.hutool.poi.ofd; \ No newline at end of file From 59d155e00f74ab3133a00a45ebd16af850aee793 Mon Sep 17 00:00:00 2001 From: Looly Date: Sat, 12 Dec 2020 14:34:17 +0800 Subject: [PATCH 38/47] fix code --- .../java/cn/hutool/poi/ofd/OfdWriter.java | 49 +++++++++++++++++-- .../java/cn/hutool/poi/ofd/OfdWriterTest.java | 16 ++++++ 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 hutool-poi/src/test/java/cn/hutool/poi/ofd/OfdWriterTest.java diff --git a/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java b/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java index b0271c490..cd264faaf 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java @@ -7,29 +7,72 @@ import org.ofdrw.layout.element.Div; import org.ofdrw.layout.element.Paragraph; import java.io.Closeable; +import java.io.File; +import java.io.OutputStream; import java.io.Serializable; import java.nio.file.Path; +/** + * OFD文件生成器 + * + * @author looly + * @since 5.5.3 + */ public class OfdWriter implements Serializable, Closeable { private static final long serialVersionUID = 1L; - private final Path destFile; private final OFDDoc doc; + /** + * 构造 + * + * @param file 生成的文件 + */ + public OfdWriter(File file){ + this(file.toPath()); + } + + /** + * 构造 + * + * @param file 生成的文件 + */ public OfdWriter(Path file){ - this.destFile = file; this.doc = new OFDDoc(file); } + /** + * 构造 + * + * @param out 需要输出的流 + */ + public OfdWriter(OutputStream out){ + this.doc = new OFDDoc(out); + } + + /** + * 增加文本内容 + * + * @param font 字体 + * @param texts 文本 + * @return this + */ public OfdWriter addText(Font font, String... texts){ final Paragraph paragraph = new Paragraph(); - paragraph.setDefaultFont(font); + if(null != font){ + paragraph.setDefaultFont(font); + } for (String text : texts) { paragraph.add(text); } return add(paragraph); } + /** + * 增加节点, + * @param div 节点,可以是段落、Canvas、Img或者填充 + * @return this + */ public OfdWriter add(Div div){ this.doc.add(div); return this; diff --git a/hutool-poi/src/test/java/cn/hutool/poi/ofd/OfdWriterTest.java b/hutool-poi/src/test/java/cn/hutool/poi/ofd/OfdWriterTest.java new file mode 100644 index 000000000..da24b5b8a --- /dev/null +++ b/hutool-poi/src/test/java/cn/hutool/poi/ofd/OfdWriterTest.java @@ -0,0 +1,16 @@ +package cn.hutool.poi.ofd; + +import cn.hutool.core.io.FileUtil; +import org.junit.Ignore; +import org.junit.Test; + +public class OfdWriterTest { + + @Test + @Ignore + public void writeTest(){ + final OfdWriter ofdWriter = new OfdWriter(FileUtil.file("d:/test/test.ofd")); + ofdWriter.addText(null, "测试文本"); + ofdWriter.close(); + } +} From 54fdf60bc39d9a088eb7927f7088950bf1937b80 Mon Sep 17 00:00:00 2001 From: Looly Date: Mon, 14 Dec 2020 00:46:48 +0800 Subject: [PATCH 39/47] add method --- .../java/cn/hutool/core/io/file/PathUtil.java | 27 +++++-- .../java/cn/hutool/poi/ofd/OfdWriter.java | 76 +++++++++++++++++-- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java index 5b7baaa07..e9877d575 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java @@ -94,8 +94,8 @@ public class PathUtil { /** * 遍历指定path下的文件并做处理 * - * @param start 起始路径,必须为目录 - * @param visitor {@link FileVisitor} 接口,用于自定义在访问文件时,访问目录前后等节点做的操作 + * @param start 起始路径,必须为目录 + * @param visitor {@link FileVisitor} 接口,用于自定义在访问文件时,访问目录前后等节点做的操作 * @see Files#walkFileTree(Path, java.util.Set, int, FileVisitor) * @since 5.5.2 */ @@ -169,7 +169,7 @@ public class PathUtil { * 通过JDK7+的 {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件 * * @param src 源文件路径,如果为目录只在目标中创建新目录 - * @param target 目标文件或目录,如果为目录使用与源文件相同的文件名 + * @param target 目标文件或目录,如果为目录使用与源文件相同的文件名 * @param options {@link StandardCopyOption} * @return Path * @throws IORuntimeException IO异常 @@ -191,14 +191,14 @@ public class PathUtil { * 拷贝文件或目录 * * @param src 源文件路径,如果为目录只在目标中创建新目录 - * @param target 目标文件或目录,如果为目录使用与源文件相同的文件名 + * @param target 目标文件或目录,如果为目录使用与源文件相同的文件名 * @param options {@link StandardCopyOption} * @return Path * @throws IORuntimeException IO异常 * @since 5.5.1 */ public static Path copy(Path src, Path target, CopyOption... options) throws IORuntimeException { - if(isFile(src, false)){ + if (isFile(src, false)) { return copyFile(src, target, options); } return copyContent(src, target.resolve(src.getFileName()), options); @@ -208,7 +208,7 @@ public class PathUtil { * 拷贝目录下的所有文件或目录到目标目录中 * * @param src 源文件路径,如果为目录只在目标中创建新目录 - * @param target 目标文件或目录,如果为目录使用与源文件相同的文件名 + * @param target 目标文件或目录,如果为目录使用与源文件相同的文件名 * @param options {@link StandardCopyOption} * @return Path * @throws IORuntimeException IO异常 @@ -227,7 +227,7 @@ public class PathUtil { * 判断是否为目录,如果file为null,则返回false
      * 此方法不会追踪到软链对应的真实地址,即软链被当作文件 * - * @param path {@link Path} + * @param path {@link Path} * @return 如果为目录true * @since 5.5.1 */ @@ -487,4 +487,17 @@ public class PathUtil { public static boolean isSymlink(Path path) { return Files.isSymbolicLink(path); } + + /** + * 判断文件或目录是否存在 + * + * @param path 文件 + * @param isFollowLinks 是否跟踪软链(快捷方式) + * @return 是否存在 + * @since 5.5.3 + */ + public static boolean exists(Path path, boolean isFollowLinks) { + final LinkOption[] options = isFollowLinks ? new LinkOption[0] : new LinkOption[]{LinkOption.NOFOLLOW_LINKS}; + return Files.exists(path, options); + } } diff --git a/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java b/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java index cd264faaf..763a2ab11 100644 --- a/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java +++ b/hutool-poi/src/main/java/cn/hutool/poi/ofd/OfdWriter.java @@ -1,13 +1,19 @@ package cn.hutool.poi.ofd; +import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.file.PathUtil; import org.ofdrw.font.Font; import org.ofdrw.layout.OFDDoc; +import org.ofdrw.layout.edit.Annotation; import org.ofdrw.layout.element.Div; +import org.ofdrw.layout.element.Img; import org.ofdrw.layout.element.Paragraph; +import org.ofdrw.reader.OFDReader; import java.io.Closeable; import java.io.File; +import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.nio.file.Path; @@ -28,7 +34,7 @@ public class OfdWriter implements Serializable, Closeable { * * @param file 生成的文件 */ - public OfdWriter(File file){ + public OfdWriter(File file) { this(file.toPath()); } @@ -37,8 +43,16 @@ public class OfdWriter implements Serializable, Closeable { * * @param file 生成的文件 */ - public OfdWriter(Path file){ - this.doc = new OFDDoc(file); + public OfdWriter(Path file) { + try { + if(PathUtil.exists(file, true)){ + this.doc = new OFDDoc(new OFDReader(file), file); + } else{ + this.doc = new OFDDoc(file); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } } /** @@ -46,20 +60,20 @@ public class OfdWriter implements Serializable, Closeable { * * @param out 需要输出的流 */ - public OfdWriter(OutputStream out){ + public OfdWriter(OutputStream out) { this.doc = new OFDDoc(out); } /** * 增加文本内容 * - * @param font 字体 + * @param font 字体 * @param texts 文本 * @return this */ - public OfdWriter addText(Font font, String... texts){ + public OfdWriter addText(Font font, String... texts) { final Paragraph paragraph = new Paragraph(); - if(null != font){ + if (null != font) { paragraph.setDefaultFont(font); } for (String text : texts) { @@ -68,16 +82,62 @@ public class OfdWriter implements Serializable, Closeable { return add(paragraph); } + /** + * 追加图片 + * + * @param picFile 图片文件 + * @param width 宽度 + * @param height 高度 + * @return this + */ + public OfdWriter addPicture(File picFile, int width, int height) { + return addPicture(picFile.toPath(), width, height); + } + + /** + * 追加图片 + * + * @param picFile 图片文件 + * @param width 宽度 + * @param height 高度 + * @return this + */ + public OfdWriter addPicture(Path picFile, int width, int height) { + final Img img; + try { + img = new Img(width, height, picFile); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return add(img); + } + /** * 增加节点, + * * @param div 节点,可以是段落、Canvas、Img或者填充 * @return this */ - public OfdWriter add(Div div){ + public OfdWriter add(Div div) { this.doc.add(div); return this; } + /** + * 增加节点, + * + * @param annotation 节点,可以是段落、Canvas、Img或者填充 + * @return this + */ + public OfdWriter add(int page, Annotation annotation) { + try { + this.doc.addAnnotation(page, annotation); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + @Override public void close() { IoUtil.close(this.doc); From efa87039ef37433b860b96e83f2c0ebf19729cad Mon Sep 17 00:00:00 2001 From: unufolio Date: Mon, 14 Dec 2020 22:46:07 +0800 Subject: [PATCH 40/47] fix typo in NumberUtil --- .../java/cn/hutool/core/util/NumberUtil.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java index a300dc01e..aa8896bf1 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java @@ -41,7 +41,7 @@ public class NumberUtil { /** * 默认除法运算精度 */ - private static final int DEFAUT_DIV_SCALE = 10; + private static final int DEFAULT_DIV_SCALE = 10; /** * 0-20对应的阶乘,超过20的阶乘会超过Long.MAX_VALUE @@ -484,7 +484,7 @@ public class NumberUtil { * @return 两个参数的商 */ public static double div(float v1, float v2) { - return div(v1, v2, DEFAUT_DIV_SCALE); + return div(v1, v2, DEFAULT_DIV_SCALE); } /** @@ -495,7 +495,7 @@ public class NumberUtil { * @return 两个参数的商 */ public static double div(float v1, double v2) { - return div(v1, v2, DEFAUT_DIV_SCALE); + return div(v1, v2, DEFAULT_DIV_SCALE); } /** @@ -506,7 +506,7 @@ public class NumberUtil { * @return 两个参数的商 */ public static double div(double v1, float v2) { - return div(v1, v2, DEFAUT_DIV_SCALE); + return div(v1, v2, DEFAULT_DIV_SCALE); } /** @@ -517,7 +517,7 @@ public class NumberUtil { * @return 两个参数的商 */ public static double div(double v1, double v2) { - return div(v1, v2, DEFAUT_DIV_SCALE); + return div(v1, v2, DEFAULT_DIV_SCALE); } /** @@ -528,7 +528,7 @@ public class NumberUtil { * @return 两个参数的商 */ public static double div(Double v1, Double v2) { - return div(v1, v2, DEFAUT_DIV_SCALE); + return div(v1, v2, DEFAULT_DIV_SCALE); } /** @@ -540,7 +540,7 @@ public class NumberUtil { * @since 3.1.0 */ public static BigDecimal div(Number v1, Number v2) { - return div(v1, v2, DEFAUT_DIV_SCALE); + return div(v1, v2, DEFAULT_DIV_SCALE); } /** @@ -551,7 +551,7 @@ public class NumberUtil { * @return 两个参数的商 */ public static BigDecimal div(String v1, String v2) { - return div(v1, v2, DEFAUT_DIV_SCALE); + return div(v1, v2, DEFAULT_DIV_SCALE); } /** From a45c92bbe4dd3c80f0d3c24b1992fac43b6fa839 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 15 Dec 2020 09:49:25 +0800 Subject: [PATCH 41/47] add PassAuth --- .../java/cn/hutool/core/net/PassAuth.java | 41 +++++++++++++++++++ .../main/java/cn/hutool/http/HttpRequest.java | 4 +- .../java/cn/hutool/http/HttpRequestTest.java | 26 ++++++++++-- 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/PassAuth.java diff --git a/hutool-core/src/main/java/cn/hutool/core/net/PassAuth.java b/hutool-core/src/main/java/cn/hutool/core/net/PassAuth.java new file mode 100644 index 000000000..d87177355 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/PassAuth.java @@ -0,0 +1,41 @@ +package cn.hutool.core.net; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; + +/** + * 账号密码形式的{@link Authenticator} 实现。 + * + * @author looly + * @since 5.5.3 + */ +public class PassAuth extends Authenticator { + + /** + * 创建账号密码形式的{@link Authenticator} 实现。 + * + * @param user 用户名 + * @param pass 密码 + * @return PassAuth + */ + public static PassAuth of(String user, char[] pass) { + return new PassAuth(user, pass); + } + + private final PasswordAuthentication auth; + + /** + * 构造 + * + * @param user 用户名 + * @param pass 密码 + */ + public PassAuth(String user, char[] pass) { + auth = new PasswordAuthentication(user, pass); + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return auth; + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java index daba627c1..e52e3fef9 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java @@ -1094,9 +1094,9 @@ public class HttpRequest extends HttpBase { } /** - * 调用转发,如果需要转发返回转发结果,否则返回null + * 调用转发,如果需要转发返回转发结果,否则返回{@code null} * - * @return {@link HttpResponse},无转发返回 null + * @return {@link HttpResponse},无转发返回 {@code null} */ private HttpResponse sendRedirectIfPossible() { if (this.maxRedirectCount < 1) { diff --git a/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java b/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java index 2dc9e487d..66a37197f 100644 --- a/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java @@ -4,14 +4,13 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.core.lang.Console; import cn.hutool.core.util.CharsetUtil; -import cn.hutool.http.HttpRequest; -import cn.hutool.http.HttpResponse; -import cn.hutool.http.HttpUtil; import cn.hutool.http.ssl.SSLSocketFactoryBuilder; import cn.hutool.json.JSONUtil; import org.junit.Ignore; import org.junit.Test; +import java.net.Authenticator; +import java.net.PasswordAuthentication; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -144,4 +143,25 @@ public class HttpRequestTest { Console.log(execute.body()); } + @Test + public void getByProxy(){ + System.setProperty("jdk.http.auth.tunneling.disabledSchemes", ""); + + // 用户名密码, 若已添加白名单则不需要添加 + final String ProxyUser = "t10757311156848"; + final String ProxyPass = "ikm5uu44"; + + Authenticator.setDefault(new Authenticator() { + public PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(ProxyUser, ProxyPass.toCharArray()); + } + }); + + final HttpResponse res = HttpRequest.get("https://httpbin.org/get") + .basicAuth(ProxyUser, ProxyPass) + .setHttpProxy("tps193.kdlapi.com", 15818).execute(); + + Console.log(res.body()); + } + } From 8dd004f4ee8635d1e1b3be6ef273face3afc9273 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 15 Dec 2020 10:12:34 +0800 Subject: [PATCH 42/47] fix spell --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8073a66c7..47f593e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * 【core 】 增加NioUtil * 【core 】 增加GanymedUtil * 【poi 】 增加OFD支持,OfdWriter +* 【poi 】 修复NumberUtil属性拼写错误(pr#1311@Github) ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) From 273482c4cbf498525aaaf8a882f46ba4364af433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=8A=E5=A4=95=E4=BD=95=E6=B1=82?= <417708459@qq.com> Date: Tue, 15 Dec 2020 14:37:50 +0800 Subject: [PATCH 43/47] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hutool-http/src/main/java/cn/hutool/http/HttpUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java index 72527f5c3..d2b6427c3 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java @@ -404,7 +404,7 @@ public class HttpUtil { */ private static HttpResponse requestDownloadFile(String url, File destFile, int timeout) { Assert.notBlank(url, "[url] is blank !"); - Assert.notNull(url, "[destFile] is null !"); + Assert.notNull(destFile, "[destFile] is null !"); final HttpResponse response = HttpRequest.get(url).timeout(timeout).executeAsync(); if (response.isOk()) { From ec9eee4aed581f3c6a3d199903d49f07765b87c0 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 15 Dec 2020 17:17:54 +0800 Subject: [PATCH 44/47] fix comment and add method --- .../src/main/java/cn/hutool/http/HttpRequest.java | 11 +++++++++++ .../main/java/cn/hutool/json/InternalJSONUtil.java | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java index e52e3fef9..22efdeef7 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java @@ -1008,6 +1008,17 @@ public class HttpRequest extends HttpBase { return proxyAuth(HttpUtil.buildBasicAuth(username, password, charset)); } + /** + * 令牌验证,生成的头类似于:"Authorization: Bearer XXXXX",一般用于JWT + * + * @param token 令牌内容 + * @return HttpRequest + * @since 5.5.3 + */ + public HttpRequest bearerAuth(String token) { + return auth("Bearer " + token); + } + /** * 验证,简单插入Authorization头 * diff --git a/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java index 177d97b06..95b8099c3 100644 --- a/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java +++ b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java @@ -74,7 +74,7 @@ final class InternalJSONUtil { * 缩进,使用空格符 * * @param writer writer - * @param indent 随进空格数 + * @param indent 缩进空格数 * @throws IOException IO异常 */ protected static void indent(Writer writer, int indent) throws IOException { From 62f1a0e36eab99139c92c6b6bac9dc2cab585471 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 15 Dec 2020 17:54:56 +0800 Subject: [PATCH 45/47] fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f593e0f..9aa12e465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ * 【core 】 修复FileUtil.loopFiles使用FileFilter无效问题(issue#I28V48@Gitee) * 【extra 】 修复JschUtil.execByShell返回空的问题(issue#1067@Github) * 【poi 】 修复特殊的excel使用sax读取时未读到值的问题(issue#1303@Github) +* 【http 】 修复HttpUtil类条件判断错误(pr#232@Gitee) ------------------------------------------------------------------------------------------------------------- From 7c3716bf6678fc04e2b1f5036cfb62726b545613 Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 16 Dec 2020 00:00:29 +0800 Subject: [PATCH 46/47] add method --- CHANGELOG.md | 1 + .../java/cn/hutool/core/convert/Convert.java | 32 +++++++++---------- .../main/java/cn/hutool/core/map/MapUtil.java | 30 +++++++++++++++++ 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aa12e465..9b7c1cd2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * 【core 】 增加GanymedUtil * 【poi 】 增加OFD支持,OfdWriter * 【poi 】 修复NumberUtil属性拼写错误(pr#1311@Github) +* 【core 】 MapUtil增加getQuietly方法(issue#I29IWO@Gitee) ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java b/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java index 052ae30e7..06d70e621 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java @@ -49,7 +49,7 @@ public class Convert { /** * 转换为字符串
      - * 如果给定的值为null,或者转换失败,返回默认值null
      + * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -85,7 +85,7 @@ public class Convert { /** * 转换为字符
      - * 如果给定的值为null,或者转换失败,返回默认值null
      + * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -108,7 +108,7 @@ public class Convert { /** * 转换为byte
      - * 如果给定的值为null,或者转换失败,返回默认值
      + * 如果给定的值为{@code null},或者转换失败,返回默认值
      * 转换失败不会报错 * * @param value 被转换的值 @@ -121,7 +121,7 @@ public class Convert { /** * 转换为byte
      - * 如果给定的值为null,或者转换失败,返回默认值null
      + * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -155,7 +155,7 @@ public class Convert { /** * 转换为Short
      - * 如果给定的值为null,或者转换失败,返回默认值
      + * 如果给定的值为{@code null},或者转换失败,返回默认值
      * 转换失败不会报错 * * @param value 被转换的值 @@ -168,7 +168,7 @@ public class Convert { /** * 转换为Short
      - * 如果给定的值为null,或者转换失败,返回默认值null
      + * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -204,7 +204,7 @@ public class Convert { /** * 转换为Number
      - * 如果给定的值为空,或者转换失败,返回默认值null
      + * 如果给定的值为空,或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -240,7 +240,7 @@ public class Convert { /** * 转换为int
      - * 如果给定的值为null,或者转换失败,返回默认值null
      + * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -275,7 +275,7 @@ public class Convert { /** * 转换为long
      - * 如果给定的值为null,或者转换失败,返回默认值null
      + * 如果给定的值为{@code null},或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -310,7 +310,7 @@ public class Convert { /** * 转换为double
      - * 如果给定的值为空,或者转换失败,返回默认值null
      + * 如果给定的值为空,或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -345,7 +345,7 @@ public class Convert { /** * 转换为Float
      - * 如果给定的值为空,或者转换失败,返回默认值null
      + * 如果给定的值为空,或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -380,7 +380,7 @@ public class Convert { /** * 转换为boolean
      - * 如果给定的值为空,或者转换失败,返回默认值null
      + * 如果给定的值为空,或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -415,7 +415,7 @@ public class Convert { /** * 转换为BigInteger
      - * 如果给定的值为空,或者转换失败,返回默认值null
      + * 如果给定的值为空,或者转换失败,返回默认值{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -480,7 +480,7 @@ public class Convert { /** * 转换为LocalDateTime
      - * 如果给定的值为空,或者转换失败,返回null
      + * 如果给定的值为空,或者转换失败,返回{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -506,7 +506,7 @@ public class Convert { /** * 转换为Date
      - * 如果给定的值为空,或者转换失败,返回null
      + * 如果给定的值为空,或者转换失败,返回{@code null}
      * 转换失败不会报错 * * @param value 被转换的值 @@ -534,7 +534,7 @@ public class Convert { /** * 转换为Enum对象
      - * 如果给定的值为空,或者转换失败,返回默认值null
      + * 如果给定的值为空,或者转换失败,返回默认值{@code null}
      * * @param 枚举类型 * @param clazz Enum的Class diff --git a/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java b/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java index 867f23b42..af3bb89b6 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java @@ -1160,6 +1160,21 @@ public class MapUtil { return null == map ? null : Convert.convert(type, map.get(key), defaultValue); } + /** + * 获取Map指定key的值,并转换为指定类型,此方法在转换失败后不抛异常,返回null。 + * + * @param 目标值类型 + * @param map Map + * @param key 键 + * @param type 值类型 + * @param defaultValue 默认值 + * @return 值 + * @since 5.5.3 + */ + public static T getQuietly(Map map, Object key, Class type, T defaultValue) { + return null == map ? null : Convert.convertQuietly(type, map.get(key), defaultValue); + } + /** * 获取Map指定key的值,并转换为指定类型 * @@ -1189,6 +1204,21 @@ public class MapUtil { return null == map ? null : Convert.convert(type, map.get(key), defaultValue); } + /** + * 获取Map指定key的值,并转换为指定类型,转换失败后返回null,不抛异常 + * + * @param 目标值类型 + * @param map Map + * @param key 键 + * @param type 值类型 + * @param defaultValue 默认值 + * @return 值 + * @since 5.5.3 + */ + public static T getQuietly(Map map, Object key, TypeReference type, T defaultValue) { + return null == map ? null : Convert.convertQuietly(type, map.get(key), defaultValue); + } + /** * 重命名键
      * 实现方式为一处然后重新put,当旧的key不存在直接返回
      From 873c5723e8a945f343dfacc317b3a04dc4892867 Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 16 Dec 2020 00:01:20 +0800 Subject: [PATCH 47/47] release 5.5.3 --- hutool-all/pom.xml | 2 +- hutool-aop/pom.xml | 2 +- hutool-bloomFilter/pom.xml | 2 +- hutool-bom/pom.xml | 2 +- hutool-cache/pom.xml | 2 +- hutool-captcha/pom.xml | 2 +- hutool-core/pom.xml | 2 +- hutool-cron/pom.xml | 2 +- hutool-crypto/pom.xml | 2 +- hutool-db/pom.xml | 2 +- hutool-dfa/pom.xml | 2 +- hutool-extra/pom.xml | 2 +- hutool-http/pom.xml | 2 +- hutool-json/pom.xml | 2 +- hutool-log/pom.xml | 2 +- hutool-poi/pom.xml | 2 +- hutool-script/pom.xml | 2 +- hutool-setting/pom.xml | 2 +- hutool-socket/pom.xml | 2 +- hutool-system/pom.xml | 2 +- pom.xml | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/hutool-all/pom.xml b/hutool-all/pom.xml index ae2daea7e..8c3103403 100644 --- a/hutool-all/pom.xml +++ b/hutool-all/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-all diff --git a/hutool-aop/pom.xml b/hutool-aop/pom.xml index a5db73731..4b1c5e526 100644 --- a/hutool-aop/pom.xml +++ b/hutool-aop/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-aop diff --git a/hutool-bloomFilter/pom.xml b/hutool-bloomFilter/pom.xml index e1ad8127b..712cda75d 100644 --- a/hutool-bloomFilter/pom.xml +++ b/hutool-bloomFilter/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-bloomFilter diff --git a/hutool-bom/pom.xml b/hutool-bom/pom.xml index 2e1791ca9..4b721b72f 100644 --- a/hutool-bom/pom.xml +++ b/hutool-bom/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-bom diff --git a/hutool-cache/pom.xml b/hutool-cache/pom.xml index d24ef2f04..49ed9b7f9 100644 --- a/hutool-cache/pom.xml +++ b/hutool-cache/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-cache diff --git a/hutool-captcha/pom.xml b/hutool-captcha/pom.xml index 1ae88a98f..8f52eeaf5 100644 --- a/hutool-captcha/pom.xml +++ b/hutool-captcha/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-captcha diff --git a/hutool-core/pom.xml b/hutool-core/pom.xml index 3680e13a0..5417ec209 100644 --- a/hutool-core/pom.xml +++ b/hutool-core/pom.xml @@ -17,7 +17,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-core diff --git a/hutool-cron/pom.xml b/hutool-cron/pom.xml index 7ea67cc03..ea131479a 100644 --- a/hutool-cron/pom.xml +++ b/hutool-cron/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-cron diff --git a/hutool-crypto/pom.xml b/hutool-crypto/pom.xml index 37a05cd4c..e2ed08f64 100644 --- a/hutool-crypto/pom.xml +++ b/hutool-crypto/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-crypto diff --git a/hutool-db/pom.xml b/hutool-db/pom.xml index b4afce592..cf326dac6 100644 --- a/hutool-db/pom.xml +++ b/hutool-db/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-db diff --git a/hutool-dfa/pom.xml b/hutool-dfa/pom.xml index 61e62b9b7..34060c810 100644 --- a/hutool-dfa/pom.xml +++ b/hutool-dfa/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-dfa diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml index 646a16e3f..91b17ef79 100644 --- a/hutool-extra/pom.xml +++ b/hutool-extra/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-extra diff --git a/hutool-http/pom.xml b/hutool-http/pom.xml index fd3c92669..136dce963 100644 --- a/hutool-http/pom.xml +++ b/hutool-http/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-http diff --git a/hutool-json/pom.xml b/hutool-json/pom.xml index e4b4f15e5..08a235a7a 100644 --- a/hutool-json/pom.xml +++ b/hutool-json/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-json diff --git a/hutool-log/pom.xml b/hutool-log/pom.xml index 99d90c35f..dee06f766 100644 --- a/hutool-log/pom.xml +++ b/hutool-log/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-log diff --git a/hutool-poi/pom.xml b/hutool-poi/pom.xml index d82e04e0e..8a53c9df4 100644 --- a/hutool-poi/pom.xml +++ b/hutool-poi/pom.xml @@ -8,7 +8,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-poi diff --git a/hutool-script/pom.xml b/hutool-script/pom.xml index fb5c71259..d9ba86df5 100644 --- a/hutool-script/pom.xml +++ b/hutool-script/pom.xml @@ -8,7 +8,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-script diff --git a/hutool-setting/pom.xml b/hutool-setting/pom.xml index ccedbb261..418fe7227 100644 --- a/hutool-setting/pom.xml +++ b/hutool-setting/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-setting diff --git a/hutool-socket/pom.xml b/hutool-socket/pom.xml index 60d88187e..c1b37b679 100644 --- a/hutool-socket/pom.xml +++ b/hutool-socket/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-socket diff --git a/hutool-system/pom.xml b/hutool-system/pom.xml index 1772b1bdd..39fa8e271 100644 --- a/hutool-system/pom.xml +++ b/hutool-system/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool-system diff --git a/pom.xml b/pom.xml index 66c86ae69..0f479c9ee 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ cn.hutool hutool-parent - 5.5.3-SNAPSHOT + 5.5.3 hutool Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。 https://github.com/looly/hutool