358 Commits

Author SHA1 Message Date
cdd08d01d8 build: 添加flatten-maven-plugin插件用于生成完整的POM文件 2026-05-28 23:41:04 +08:00
cc6f599ddf chore: 移除冗余配置并更新依赖版本
- 移除了子模块的 pom.xml 文件中的冗余配置项,包括重复的 url、licenses、developers 和 scm 配置
- 移除了 checker-qual依赖
- 清理了多余的maven插件配置
- 更新了多个依赖库的版本,详情如下
    - guava: 33.5.0-jre → 33.6.0-jre
    - joda-time: 2.14.0 → 2.14.2
    - commons-io: 2.21.0 → 2.22.0
    - okio: 3.16.4 → 3.17.0
    - logback: 1.3.15 → 1.3.16
    - jackson: 2.18.3 → 2.21.3
    - gson: 2.13.2 → 2.14.0
    - byte-buddy: 1.18.3 → 1.18.8
    - java-jwt: 4.5.0 → 4.5.2
    - lombok: 1.18.42 → 1.18.46
    - hutool: 5.8.43 → 5.8.46
    - junit: 5.14.2 → 5.14.4
2026-05-28 22:35:48 +08:00
1b7e0f5a40 docs: 更新作者信息并完善Javadoc文档 2026-05-28 21:51:39 +08:00
a6436cde85 chore: 更新项目版本号为SNAPSHOT 2026-05-28 21:15:16 +08:00
620546ccda release/1.1.0-RC1 [#72 (Gitea)] 2026-05-28 21:11:49 +08:00
d5cf06bee5 docs: 更新 README.md 2026-05-28 20:58:38 +08:00
1e5b4b574b chore: 更新版权信息 2026-05-28 20:56:34 +08:00
3462e5340f chore: 更新版本号并完善项目元数据配置
- 将版本从 1.1.0-SNAPSHOT 更新为 1.1.0-RC1
- 为 plusone-commons、plusone-dependencies 和父 pom 添加项目名称
- 配置项目 URL、许可证、开发者和 SCM 信息
- 在父 pom 中添加 release profile 支持 GPG 签名和中央仓库发布
2026-05-28 20:55:38 +08:00
b0685ae32f refactor: 重构 YearQuarter
- 修改 plusQuarters 的偏移量计算逻辑
- 同步修正测试中 plusQuarters 偏移量验证的相关逻辑
- 弃用 YearQuarter#of(Date):该方法隐式依赖系统默认时区,行为不可控
- 新增 YearQuarter#of(Date, ZoneId) 和 YearQuarter#of(Date, TimeZone) 工厂方法
2026-05-28 15:20:18 +08:00
8cc11da121 refactor: 移除 ValidatableStringRecordTests 中的抑制注解 2026-05-28 15:20:18 +08:00
f2b9beb873 chore: 优化 Nullability Annotations 使用 2026-05-28 15:20:18 +08:00
84112fcf45 perf: 优化 MapModifier 实现 2026-05-28 15:20:18 +08:00
f7518063f8 refactor!: 调整 PagingAndSortingQueryParams 成员的可见性 2026-05-03 13:51:02 +08:00
4b7390447b docs: 更新代码注释中的文档链接和描述 2026-05-03 13:47:52 +08:00
bed2f75da2 refactor: 添加@Nonnull注解以增强代码的空值安全性检查。 2026-05-03 13:47:52 +08:00
c1e67af382 build: 整理 pom.xml
- 将 Java 版本配置、maven 编译器插件配置以及依赖版本属性从子模块移至根 pom.xml 进行统一管理
- 升级依赖版本
2026-04-13 22:30:30 +08:00
0ec65bf39d docs: 完善文档 2026-04-13 22:27:13 +08:00
c6f9cc0a80 refactor!: 使用 JdbcUpdateAffectedIncorrectNumberOfRowsException 取代 DataOperationResultException
- `DataOperationResultException` 重命名为 `JdbcUpdateAffectedIncorrectNumberOfRowsException`
- `AssertTools` 中的 `checkAffectedRows` 相关方法移到 `JdbcUpdateAffectedIncorrectNumberOfRowsException` 中
2026-04-13 22:25:18 +08:00
829a7ed798 docs: 更新项目描述信息 2025-10-24 17:13:36 +08:00
f492d5d62e docs: 重命名文档文件名 2025-10-24 17:10:17 +08:00
159a7769dc docs: 修改文档目录 2025-10-24 17:02:57 +08:00
9ab92ce471 docs: 完善项目文档 [!9 (gitee)]
修改包描述(package-info.java)
修改 README.md
添加 docs 文件夹,包含各部分功能的介绍
2025-10-24 03:18:37 +00:00
8dfb3ff694 refactor!: 将 Ref 类从 base 包移动到 util 2025-10-23 16:59:04 +08:00
264717eb62 docs: 修复 UnifiedResponses 的 javadoc 错误 2025-10-23 16:26:11 +08:00
ba38175d93 refactor!: 优化 PagingAndSortingQueryParams (!7 @Gitee)
`PagingAndSortingQueryParams` 的构造器中必须传入一个 `PagingParamsBuilder`,
构建分页参数时使用此 `PagingParamsBuilder`。

只要子类确保同一场景下共用一个  `PagingParamsBuilder`  实例,就不会导致白名单重复校验。
2025-10-23 08:17:17 +00:00
b8c666a023 test: 使用 lombok 简化 TreeBuilder 的测试代码 2025-10-22 17:50:09 +08:00
255aaf182a feat: 新增 MapModifier 用于链式操作修改 Map 数据 (#71 @Gitea)
1. 添加 `MapModifier` 类,封装了一系列对 Map 数据的修改过程,如 `put`、`putIfAbsent`、`putAll`、`remove`、`clear` 等
2. 支持直接修改指定的 `Map`
3. 支持从 `Supplier` 中获取 `Map` 并进行修改
4. 提供了创建并初始化 unmodifiableMap 的方法
5. 增加了相应的单元测试用例

issue Close #67 @Gitea
2025-10-22 09:37:04 +08:00
468453781e chore: add lincese header 2025-10-21 16:32:29 +08:00
3b241de08c docs: 修改 UnifiedResponses 相关文档 (!5 @Gitee) 2025-10-21 07:16:24 +00:00
fb46def402 update NOTICE.
Signed-off-by: ZhouXY108 <luquanlion@outlook.com>
2025-10-21 05:25:00 +00:00
386ede6afd add NOTICE.
Signed-off-by: ZhouXY108 <luquanlion@outlook.com>
2025-10-21 05:23:42 +00:00
8ec61a84c9 build: 添加 MinIO 依赖版本管理 2025-10-20 23:16:03 +08:00
726a94a1a8 chore: 添加依赖管理注释 2025-10-20 23:15:24 +08:00
3b441e4575 docs: update README.md
Signed-off-by: ZhouXY108 <luquanlion@outlook.com>
2025-10-17 10:25:54 +00:00
4ed6edd9b6 feat(model): 添加 SemVer 表示语义版本号 (!4 @Gitee)
- feat: 添加 `SemVer` 表示语义版本号
- test: 完善 `SemVer` 的单元测试
2025-10-17 18:13:29 +08:00
3d297331c4 test: 减少 ZipTools 测试时处理的数据量 2025-10-17 10:16:52 +08:00
665de3cdf0 docs: 更新 IdWorker 的文档注释 (!3 @Gitee) 2025-10-14 13:12:04 +00:00
c2862203d4 feat: 新增 RandomTools#randomInt 用于生成指定区间内的随机整数 (!2 @Gitee) 2025-10-14 02:00:04 +00:00
fafb26fcf7 test: 减少测试时处理的数据量 2025-10-14 02:37:03 +08:00
a65af967cb feat: 新增 ZipTools 工具类 (#70 #Gitea)
新增 `ZipTools` 工具类提供最基础的数据压缩/解压方法
2025-10-13 21:21:15 +08:00
27e5650482 perf: 优化 AssertTools 代码 2025-10-13 17:36:47 +08:00
8106126e79 refactor!: 重构 PagingAndSortingQueryParams (#69 @Gitea)
将构建 `PagingParams` 的过程放在 `PagingParamsBuilder` 中。
当定义一个 `PagingAndSortingQueryParams` 的子类时,
该类应包含一个静态的 `PagingParamsBuilder` 单例对象,
使对 `sortableProperties` 的校验逻辑在类加载时执行。

原来实现方式是将校验 `sortableProperties` 的逻辑放在
`PagingAndSortingQueryParams` 的构造方法中,
每创建一个对象就得执行一次,造成不必要的浪费。
2025-10-12 01:33:18 +08:00
99d8f210c5 feat(dependencies): 添加 okhttp 和 okio 依赖 2025-10-09 10:41:24 +08:00
08432353fe refactor(exception)!: 修改 SysExceptionBizException
- 将 `SysException` 和 `BizException` 的构造方法设为 `protected`,供子类的构造方法调用
- 创建对应的工厂方法用于直接创建 `SysException` 和 `BizException` 实例
2025-10-01 22:56:56 +08:00
a7b1067ebb test: 完善 Numbers#sum(BigInteger...) 的单元测试 2025-09-03 21:40:51 +08:00
1dbfb36146 feat: Numbers 类新增 parseXxx 方法 (#65 @Gitea)
`Numbers` 类新增 `parseXxx` 方法用于将字符串转为对应类型的数字,当转换失败时返回默认值。默认值允许为 `null`。
2025-09-03 21:37:42 +08:00
1fc0b198c9 feat: 新增将日期区间转为日期时间区间的方法 (#64 @Gitea)
- 重载 `DateTimeTools#toDateTimeRange`
- 对新增的方法进行单元测试
- 简化单元测试代码
2025-09-03 21:08:51 +08:00
15e07901e6 perf: 简单优化代码 (#63 @Gitea) 2025-09-03 21:01:50 +08:00
8f451e7eb9 refactor(exception)!: 重构多场景异常相关代码 (#62 @Gitea)
- `IExceptionType` 不继承自 `IExceptionFactory`,具体表示异常场景的枚举,可按需实现这两个接口
- 简化 `IMultiTypesException` 接口定义,不与 `IExceptionType` 强制绑定
- 修改相关文档与描述

通过以上修改,使表示异常场景的枚举可以与异常类分开定义,使不同的异常可以复用同一套场景枚举。不强制作为单一异常的工厂,在被不同的异常复用时,可以更灵活地定义不同的工厂方法。
2025-09-03 20:41:23 +08:00
2b6f946759 perf: 优化 JodaTimeTools (#61 @Gitea)
- 优化 `JodaTimeTools`
- 完善 javadoc
2025-09-03 19:49:12 +08:00
ce9f3edfbc build: 保持开发分支的版本号为 SNAPSHOT 2025-08-01 11:31:55 +08:00
0f90756f44 release: 1.1.0-RC2 2025-07-31 11:14:10 +08:00
34a49d30ca chore: 更新代码仓库地址 (plusone/plusone-commons#60 @Gitea)
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-07-25 10:27:10 +08:00
f4c3793aab chore: 修改 @author 信息 (#59@Gitea)
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-07-25 10:23:26 +08:00
6556a53163 refactor!: 重构 MultiTypesException (#58@Gitea)
- 将 `MultiTypesException` 重命名为 `IMultiTypesException`
- 将 `ExceptionType` 重命名为 `IExceptionType`
- 将 `IExceptionType` 中的工厂方法抽取到 `IExceptionFactory` 中
- 在 `IMultiTypesException` 接口中添加泛型参数 `TCode`,用于指定异常类型代码的类型
- 在 `IExceptionType` 接口中添加 `getDescription` 方法,用于获取异常类型的描述信息

Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-07-25 09:28:59 +08:00
56079c29d8 refactor!: 将 ParsingFailureException 改为受检异常 2025-07-22 14:58:04 +08:00
f111b02c21 build: 将 plusone-dependencies 版本更新为项目版本
- 将 plusone-dependencies 的固定版本号替换为 ${project.version}
- 该修改确保了依赖版本与项目版本的一致性
2025-07-22 14:58:00 +08:00
56fd5f0a6a refactor!: 将 JodaTime 相关方法从 DateTimeTools 类中提取到新的 JodaTimeTools 类 2025-07-22 14:57:34 +08:00
e2e5f50162 refactor: AssertTools 类中的方法使用静态导入 2025-06-12 16:11:53 +08:00
8eac9054cd refactor(gson): 重构 JSR310TypeAdapters
- 抽象出 TemporalAccessorTypeAdapter 类,简化了 LocalDate、LocalDateTime、ZonedDateTime 和 Instant 类型适配器的实现
2025-06-09 17:55:25 +08:00
a55c712349 test: 使用 JSR310TypeAdapters 简化测试代码 2025-06-09 17:21:26 +08:00
0eda94a658 refactor(exception): 为异常类添加 serialVersionUID
为以下异常类添加 serialVersionUID 字段:
- ParsingFailureException
- BizException
- InvalidInputException
- RequestParamsException
- DataOperationResultException
- SysException
2025-06-09 17:05:10 +08:00
c816696c55 docs: 改正 ParsingFailureException 文档注释中的错误描述 2025-06-09 16:14:44 +08:00
8828b12c78 release: 1.1.0-RC1 2025-06-08 13:38:03 +08:00
89acbecc5a docs: 修改 javadoc 中的格式错误 2025-06-08 13:08:45 +08:00
336d99d4ba feat(gson): 添加 Gson 适配器以支持 JSR-310 中常用的类
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/57
2025-06-07 01:02:15 +08:00
0731bf2c22 refactor(gson): 合并多个 JSR-310 类型适配器到 JSR310TypeAdapters
- 将 InstantTypeAdapter、LocalDateTimeTypeAdapter、LocalDateTypeAdapter 和 ZonedDateTimeTypeAdapter 合并到 JSR310TypeAdapters 类中
- 更新包结构,移动适配器到 adapter 子包
- 新增相关包的 javadoc
2025-06-07 00:56:11 +08:00
2827f69aef feat(gson): 新增 InstantTypeAdapter 用于 Gson 序列化和反序列化 Instant
`InstantTypeAdapter` 用于 Gson 通过 `DateTimeFormatter#ISO_INSTANT` 序列化和反序列化 `Instant` 类型。
2025-06-07 00:22:04 +08:00
2e73ca5f6d feat(gson): 添加 Gson 适配器以支持 Java 8 日期时间 API
- 新增 `LocalDateTimeTypeAdapter`、`LocalDateTypeAdapter` 和 `ZonedDateTimeTypeAdapter`
- 将 Gson 作为可选依赖
2025-06-06 19:13:52 +08:00
1239a11cd7 refactor: 优化 UnifiedResponses 工厂方法的泛型定义
将不指定 data 的工厂方法也改成泛型方法,而不是返回 `UnifiedResponse<Void>`。
2025-06-06 11:40:20 +08:00
f8a2046d2d refactor!: 将 RegexToolsmatchesOne 方法重命名为 matchesAny 2025-06-04 17:12:29 +08:00
fb2036c038 build: 升级 logback 到 1.3.15
logback 1.3.x 是最后一个支持 JDK8 的版本。

如果使用 Spring Boot,建议使用 Spring Boot 绑定的版本,但是 Spring Boot 2.7.x 最高只支持 logback 1.2.x,所以不可避免使用的有漏洞的版本。
2025-05-28 21:14:27 +08:00
f9b4c3c58c feat: 新增 StringTools#toQuotedString 方法 2025-05-18 15:18:52 +08:00
3ca2ec3be0 build: 简化依赖声明 2025-05-14 10:42:06 +08:00
f83bb55fd6 refactor!: 重构 DataOperationResultException (#56)
删除 `DataOperationResultException` 多余的构造方法,仅提供两个构造方法。
创建 `DataOperationResultException` 实例时,必须将预计影响的行数
和实际影响的行数作为入参。(不兼容)

重构 `AssertTools` 中相关的断言方法。

Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/56
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-05-09 21:45:30 +08:00
e90e3dc1b4 fix(dependencies): 改正 jasypt 版本配置
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/55
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-05-02 17:07:48 +08:00
b774d8c477 chore: 更新 Code Spell Checker 的配置
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/54
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-05-02 15:27:32 +08:00
2a18a47ffe chore: 更新 guava
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/53
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-05-02 15:09:06 +08:00
cb903a8cce docs: 修改 MultiTypesException 文档描述
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-05-02 14:13:07 +08:00
030ed9ed3b refactor: 更改项目结构
创建父项目 plusone-parent,将 plusone-commons 放在 plusone-parent 下;
在 plusone-parent 下创建 plusone-dependencies,由 plusone-dependencies 管理可能用到的所有依赖。
2025-05-02 11:31:57 +08:00
5ce738bdfc docs: 完善 javadoc 2025-05-01 03:46:03 +08:00
97a4ae2279 perf: RegexTools 的缓存改用 guava cache 2025-05-01 02:15:23 +08:00
af66cd2380 feat: RegexTools 新增重载方法,当将字符串视为正则表达式入参时,允许传对应的 flags 2025-05-01 02:08:23 +08:00
3b519105bf refactor!: 删除 RegexTools 中以 String[] 作为多个正则表达式入参的方法
字符串无法代表一个正则表达式,还需考虑正则表达式的 flag(s),所以当使用多个正则表达式时,更推荐使用 `Pattern[]`。
2025-04-30 22:57:47 +08:00
b70e526509 docs: 修改段落问题
Signed-off-by: zhouxy108 <luquanlion@outlook.com>
2025-04-30 22:47:49 +08:00
a2781012be feat: 在 AssertTools 中新增 checkArgumentNotNull 系列方法 2025-04-30 22:44:29 +08:00
9e410029b1 refactor: 修改 AssertTools 中的参数名称 2025-04-30 22:17:18 +08:00
ee7213a687 refactor(util): ArrayTools 中的 isNullOrEmpty 重命名为 isEmpty
保持方法命名的一致性

BREAKING CHANGE: `ArrayTools#isNullOrEmpty` 重命名为 `ArrayTools#isEmpty`
2025-04-29 11:26:12 +08:00
45dc105dd0 fix: 修复 JDK17+ 环境下测试用例 PagingAndSortingQueryParamsTests#testGson 不通过的问题
该用例在 JDK17+ 环境下使用 gson 进行序列化时,报 `com.google.gson.JsonIOException: Failed making field 'java.time.LocalDateTime#date' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type`。

See: https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible
2025-04-16 14:52:16 +08:00
c779430e6f chore: 更新依赖
依赖:
- guava: `33.4.0-jre` 更新到 `33.4.2-jre`;
- joda-time: `2.13.0` 更新到 `2.14.0`;

*测试依赖:
- junit: `5.11.4` 更新到 `5.12.1`;
- hutool: `5.8.35` 更新到 `5.8.37`;
- jackson: `2.18.2` 更新到 `2.18.3`;
2025-04-16 14:32:12 +08:00
14b193418d docs: fix param name
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/43
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-04-11 14:42:01 +08:00
56a4a606a6 refactor: 优化 OptionalTools 代码 2025-04-09 22:11:21 +08:00
7abd3a05ab refactor: UnifiedResponse 的字段为 final 2025-04-09 21:45:04 +08:00
a05fc6cfe1 style: 格式化代码 2025-04-09 21:44:59 +08:00
bca4ce531a chore: 添加 cspell 配置文件 2025-04-09 21:41:26 +08:00
4b9c0de860 chore: update copyright info (#42)
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/42
Co-authored-by: ZhouXY108 <luquanlion@outlook.com>
Co-committed-by: ZhouXY108 <luquanlion@outlook.com>
2025-04-09 18:16:55 +08:00
0f802db105 docs: 完善 javadoc 2025-04-09 18:16:54 +08:00
7606a4263c fix: 补充 ThrowingPredicate 缺失的 FunctionalInterface 注解 2025-04-09 18:16:54 +08:00
f05e804795 chore: ArrayTools 中删除已完成的 TODO 注释 2025-04-09 18:16:54 +08:00
36d05045cf refactor!: 限制 PagingAndSortingQueryParams 中用于排序的字段名称的长度,允许包含短横(-) 2025-04-09 18:16:54 +08:00
05c30109ec chore: 抑制测试代码的一些警告 2025-04-09 18:16:54 +08:00
6f26613f30 refactor!: 重命名 ArrayTools 中的方法
`indexOfWithPredicate` 重命名为 `indexOf`,
`lastIndexOfWithPredicate` 重命名为 `lastIndexOf`。
2025-04-09 18:16:54 +08:00
9ad82bdb57 refactor!: RegexTools 中使用 ArrayTools#isAllElementsNotNull 判断数组
BREAKING CHANGE: 数组为 `null` 时,不抛出 `NullPointerException`,而是 `IllegalArgumentException`。
2025-04-09 18:16:54 +08:00
7babf0953a refactor: 重构 Chinese2ndGenIDCardNumber
不使用 ValidatableStringRecord,在工厂方法中进行参数校验。
2025-04-09 18:16:54 +08:00
09c6f41610 chore: 弃用 ValidatableStringRecord 2025-04-09 18:16:54 +08:00
eda835996e refactor: IDCardNumber#toDesensitizedString 使用 StringTools#desensitize 进行脱敏 2025-04-09 18:16:54 +08:00
2396b78c4f feat: 新增字符串脱敏方法 StringTools#desensitize 2025-04-09 18:16:54 +08:00
6a498c301d docs: 完善 javadoc 2025-04-09 18:16:54 +08:00
40a44a939d chore: 优化 JSR305 注解的使用 2025-04-09 18:16:54 +08:00
7dfa93aca0 prepare 1.1.x 2025-04-09 18:16:54 +08:00
4e6028f217 docs: 介绍 UnifiedResponses 的文档中 CustomUnifiedResponseFactoryTests 链接到代码托管平台对应的页面
close plusone/plusone-commons#33
2025-04-09 18:16:54 +08:00
57d85d05e9 docs: 修改 since 信息 (plusone/plusone-commons#40)
1.0.0 之前新增的,其 since 修改为 1.0.0,统一以 1.0.0 作为初始版本

Issue: fix plusone/plusone-commons#30
2025-04-09 18:16:54 +08:00
78e44ac317 docs: 改正 PredicateTools 的 javadoc (#38)
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/38
2025-04-09 18:16:54 +08:00
96a18c4f01 docs: 删除 IdWorker 的 author 信息
该工具来自 seata,并非本项目原创,写 javadoc 时忘记修改模板中的 author,而seata 源代码中早就删除了该类的 author 信息,故应先删除。

见:https://github.com/apache/incubator-seata/pull/6179

close plusone/plusone-commons#31
2025-04-09 18:16:53 +08:00
53d2c98461 docs: 改正 OptionalTools 的 javadoc
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/34

Issue: fix plusone/plusone-commons#29
2025-04-09 18:16:04 +08:00
c82d1cf569 docs: fix typos 2025-04-09 17:50:23 +08:00
a9024e37f1 chore: 修改 copyright 2025-04-09 17:50:08 +08:00
4d0968a191 feat: StringTools 新增工具方法
新增 StringTools#isBlank、StringTools#isEmpty、StringTools#isNotEmpty、StringTools#isURL、StringTools#isEmail
2025-04-09 17:48:34 +08:00
4df399bf54 feat: DateTimeTools 新增 isFuture 和 isPast。 2025-04-09 17:48:34 +08:00
4a620e5a2b fix: 修改 email 的正则常量
1. fix: Email 的 Pattern 不区分大小写
2. docs: 标注正则表达式的出处
2025-04-09 17:48:10 +08:00
e989ad8f60 docs: 修改注释和文档 2025-04-09 17:47:05 +08:00
2f1df1b188 chore: 优化 JSR305 注解的使用 2025-04-09 17:46:44 +08:00
90da2b8eaa Quarter#fromMonth 新增判空 2025-03-22 15:05:32 +08:00
8d3bbbc56b add README.md. 2025-02-24 23:11:01 +08:00
bdd6e61160 补充各包的 javadoc
在每个包下都创建 package-info.java 文件,编写 javadoc,对每个包进行说明。
2025-02-21 21:50:18 +08:00
f024a08dd2 v1.0.0
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/28
2025-02-20 23:22:16 +08:00
15cea5fb4b v1.0.0 2025-02-20 23:14:56 +08:00
cb2eb0633f 新增 UnifiedResponses
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/27
2025-02-20 23:14:50 +08:00
ff3f80a447 更改 copyright。 2025-02-20 23:05:27 +08:00
e5a57e03b4 新增 UnifiedResponses
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/24
2025-02-20 20:51:29 +08:00
faab942e13 新增 UnifiedResponse 工厂 UnifiedResponses。 2025-02-20 20:26:21 +08:00
0f7ab8fed5 1.0.0-RC3
Merge pull request '1.0.0-RC3' (#21) from 1.x.x into dev
2025-02-14 19:03:05 +08:00
69377f3e67 1.0.0-RC3
Merge pull request '1.x.x' (#20) from ZhouXY108/plusone-commons:1.x.x into 1.x.x
2025-02-14 18:59:31 +08:00
ec4efe5f0f Merge branch '1.x.x' into 1.x.x 2025-02-14 18:58:17 +08:00
d92861284c 整理依赖版本;修改版本号 1.0.0-RC3。 2025-02-14 18:27:43 +08:00
cd88892762 IdGenerator#toSimpleString 添加 @Nonnull 注解 2025-02-14 16:58:33 +08:00
e3ff5a2ab3 CollectionTools#isNotEmpty 支持 guava 的 Table、Multimap、Multiset 和 RangeSet 2025-02-14 16:57:45 +08:00
d217e8b9ac 优化 TreeBuilder 的单元测试代码。 2025-01-30 19:23:25 +08:00
669506e499 删除 YearQuarter#max 和 YearQuarter#min,使用 guava 的 Comparators#max 和 Comparators#min 即可。 2025-01-30 19:21:21 +08:00
79b8b81220 test: 测试工具类的私有构造器 2025-01-22 21:57:53 +08:00
6a54a8628b docs: 完善 JavaDoc,修改 copyright。 2025-01-22 21:53:07 +08:00
55027d91ef 优化多类型异常的结构 2025-01-10 17:39:41 +08:00
ac0f73f7f0 UnifiedResponse 的 status 修改为 code,类型为 String;构建 error 时必须传 code。 2025-01-10 16:00:37 +08:00
0c4cfd3044 1.0.0-RC2
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/19
2025-01-07 17:18:48 +08:00
52a4011eb1 更新版本号
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/18
2025-01-07 17:15:45 +08:00
bd992905f3 更新版本号 2025-01-07 17:12:48 +08:00
0bcc5b895a 新增 ThrowingFunction
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/16
2025-01-07 17:09:52 +08:00
4c0f5095fa 新增 base 和 function 包的 package-info
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/15
2025-01-07 17:06:33 +08:00
044320a2a9 优化 Ref API
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/14
2025-01-07 17:01:57 +08:00
1c940a9c7c IWithCode 相关接口修改方法名,区分重载方法
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/13
2025-01-07 16:52:48 +08:00
4766f78411 新增 ThrowingFunction 2025-01-07 16:34:50 +08:00
0d1935bc8c 新增 function 包的 package-info 2025-01-07 16:33:29 +08:00
8813923c86 新增 base 包的 Javadoc;优化 base 包中 JSR-305 注解的使用。 2025-01-07 16:21:24 +08:00
cda624c528 在 Ref 对象内部修改 value 的方法定义为 transformValue;新增 Ref 相关方法;删除各种衍生的 XxxRef 2025-01-07 16:17:20 +08:00
6e51302ba1 IWithCode 相关接口修改方法名,区分重载方法 2025-01-06 16:16:52 +08:00
5c3923f8af 修改版本号
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/12
2025-01-01 21:10:35 +08:00
c2187a0823 修改版本号 2025-01-01 21:07:08 +08:00
0eee05d46a Merge pull request '完成单元测试' (#11) from ZhouXY108/plusone-commons:1.x.x into 1.x.x
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/11
2025-01-01 20:47:38 +08:00
9978444120 Merge pull request '完成单元测试' (#3) from unit-tests into 1.x.x
Reviewed-on: http://zhouxy.xyz:3000/ZhouXY108/plusone-commons/pulls/3
2025-01-01 20:44:14 +08:00
25161044e9 完成分页查询参数的单元测试 2025-01-01 20:37:21 +08:00
6c225513ca 更新单元测试进度 2025-01-01 17:22:42 +08:00
9f96db1a0b 完成 ArrayTools 单元测试 2025-01-01 17:17:50 +08:00
8f588c25b5 构造 PageResult 时,如果 list 是 null,则自动转为空列表,不抛异常。 2024-12-29 22:40:35 +08:00
8a60f4db66 保存 CollectionTools#nullToEmptyXXX 的单元测试。 2024-12-29 22:35:53 +08:00
f1412d6eea 更新测试进度 2024-12-29 22:25:11 +08:00
a2482419e0 完成 DateTimeTools 的单元测试和修复,并修改其它不合适的单元测试用例。 2024-12-29 22:25:11 +08:00
979eedabb1 完成正则工具相关测试。 2024-12-29 22:25:11 +08:00
1727af5940 更新 ProgressOfTesting.txt 2024-12-29 22:25:11 +08:00
8411e75274 删除意义不大的 MapWrapper 2024-12-29 22:25:11 +08:00
1f8036e170 检查并完善 BigDecimals、Numbers、OptionalTools 的单元测试。 2024-12-29 22:25:11 +08:00
37263c6933 补充 copyright 2024-12-29 22:25:11 +08:00
ab2fc54162 完成 UnifiedResponse 单元测试;新增 PageResult#empty,修改 PageResult#toString。 2024-12-29 22:25:11 +08:00
40302d83cf 删除 SQL 构造器。暂不考虑相关工具。 2024-12-29 22:25:11 +08:00
9ccaa2d1d6 更新 ProgressOfTesting.txt 2024-12-29 22:25:11 +08:00
d72a5d3255 Chinese2ndGenIDCardNumber 继承自 ValidatableStringRecord;测试 ValidatableStringRecord。 2024-12-29 22:25:11 +08:00
f1491117de 完成 RandomTools 单元测试 2024-12-29 22:25:11 +08:00
36823c1181 更新 ProgressOfTesting.txt 2024-12-29 22:25:11 +08:00
76f612f2cc 完成身份证号的单元测试 2024-12-29 22:25:11 +08:00
a887771565 完成 ID 生成器的单元测试 2024-12-29 22:25:11 +08:00
fb5ff43ed6 更新 ProgressOfTesting.txt 2024-12-29 22:25:11 +08:00
cd9a9da7ba 删除 SafeConcurrentHashMap。注意使用 ConcurrentHashMap#computeIfAbsent 方法时,mappingFunction 里不要调用该 map 的 computeIfAbsent 即可。 2024-12-29 22:25:11 +08:00
1b2978fb06 补充 ID 生成器的 Javadoc。 2024-12-29 22:25:11 +08:00
1a76f00b6a 完成 Quarter 和 YearQuarter 的单元测试 2024-12-29 22:25:11 +08:00
300a2436b1 CollectionTools 新增 nullToEmptyList、nullToEmptySet、nullToEmptyMap 方法
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/10
2024-12-29 22:22:21 +08:00
a6bb069f8a 完成 EnumTools 单元测试
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/9
2024-12-29 22:20:35 +08:00
dbb2ef8c50 修正 InvalidInputExceptionTests 和 ParsingFailureExceptionTests
Merge pull request '修正 InvalidInputExceptionTests 和 ParsingFailureExceptionTests' (#8) from ZhouXY108/plusone-commons:temp/complete-exception-tests into 1.x.x
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/8
2024-12-29 22:18:22 +08:00
f7f7bed848 CollectionTools 新增 nullToEmptyList、nullToEmptySet、nullToEmptyMap 方法
Reviewed-on: http://zhouxy.xyz:3000/ZhouXY108/plusone-commons/pulls/1
2024-12-29 22:09:31 +08:00
c8e1d9ac59 CollectionTools 新增 nullToEmptyList、nullToEmptySet、nullToEmptyMap 方法
`CollectionTools` 提供 `nullToEmptyList`、`nullToEmptySet`、`nullToEmptyMap`,分别在提供的集合为 `null` 时,返回 `Collections.emptyList()`、`Collections.emptySet()`、`Collections.emptyMap()`。

close #7
2024-12-29 22:07:05 +08:00
6bc32ce379 完成 EnumTools 单元测试 2024-12-26 11:42:13 +08:00
cee24b3d10 修正 InvalidInputExceptionTests 和 ParsingFailureExceptionTests 2024-12-26 11:38:24 +08:00
1fde8a9b8a 更新 ProgressOfTesting.txt 2024-12-25 23:23:55 +08:00
7ad911fc1e 更新 ProgressOfTesting.txt 2024-12-25 23:19:41 +08:00
6b82b49520 完成 Quarter 单元测试
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/5
2024-12-25 21:45:12 +08:00
979293cb7a 完成 Quarter 单元测试 2024-12-25 21:30:18 +08:00
767217a143 删除已完成的 TODO 2024-12-25 21:30:11 +08:00
3940f473ea 完成 AssertTools 单元测试,修复错误
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/4
2024-12-25 20:52:49 +08:00
b37263f95c 完成 AssertTools 单元测试,修复错误 2024-12-25 20:48:11 +08:00
23ab9a68ea 优化异常,并完成单元测试
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/3
2024-12-25 17:12:54 +08:00
8c1db56d65 1. 优化多类型异常的创建方式;
2. 修改参数名,不使用缩写;
3. 完成异常的单元测试,
2024-12-25 17:08:55 +08:00
129ace6878 重载 DataNotExistsException 构造器 2024-12-24 18:01:41 +08:00
4559636e7d 优化代码
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/2
2024-12-24 17:58:17 +08:00
9f7eda47fe 修改 EnumTools 中方法的参数名称 2024-12-24 17:41:22 +08:00
8ac446e228 YearQuarter 新增方法 2024-12-24 17:41:22 +08:00
c9db3828a3 使用工具类简化代码 2024-12-24 17:41:22 +08:00
4b9b38eeb9 Quarter 实现 IWithIntCode。 2024-12-24 17:41:22 +08:00
ef35833dc0 调整测试类所在包 2024-12-24 17:41:22 +08:00
102ce5185a 异常的 Type 实现 IWithCode 接口 2024-12-24 17:41:22 +08:00
672e180f43 修改变量名,不使用缩写 2024-12-24 17:41:22 +08:00
488aaad452 使用 AssertTools 替换 Preconditions。 2024-12-24 17:41:21 +08:00
1e4306005e Gender 实现 IWithIntCode 接口。 2024-12-24 17:41:21 +08:00
f93eec6d08 完成 IWithCode 相关接口和 TreeBuilder 的单元测试
Reviewed-on: http://zhouxy.xyz:3000/plusone/plusone-commons/pulls/1
2024-12-24 11:48:04 +08:00
424857df6a 完成 TreeBuilder 单元测试。 2024-12-24 11:35:02 +08:00
f6b2509b12 完成 IWithCode 相关接口的单元测试。 2024-12-24 10:44:05 +08:00
c78a2ab380 新增 YearQuarter#getQuarterValue 2024-12-24 09:31:21 +08:00
33e1c14755 1. 删除 DateTimeTools 中的格式化器的缓存,应由调用方执行负责维护;
2. 新增 toYearString 和 toMonthString 方法。
2024-12-23 00:38:20 +08:00
fbef9bd005 格式化 SQL Builder 相关代码 2024-12-23 00:36:01 +08:00
275a156184 新增 UnifiedResponse 的单元测试 2024-12-05 01:45:05 +08:00
ebacc622da 添加 MapWrapper 的注释 2024-12-04 19:37:26 +08:00
9300f02a51 修正注释拼写错误 2024-12-04 11:21:06 +08:00
ab82cfeea6 删除 ReadWriteLockedTable,使用 Table 时自己把握锁的使用。 2024-12-04 11:20:39 +08:00
2cd0640a0b 添加 CollectionTools 单元测试 2024-12-04 10:36:03 +08:00
cedf77d33c CollectionTools 添加注释 2024-12-04 10:35:39 +08:00
1e1c003751 更新测试进度 2024-12-04 01:41:43 +08:00
0e90956147 重载 equalsCode 方法;添加单元测试 2024-12-04 01:39:47 +08:00
75b39de99f 更新测试进度 2024-12-04 01:17:53 +08:00
ab318136a8 补充单元测试 2024-12-04 01:17:42 +08:00
b4115aae95 使用 AssertTools 替代 Preconditions 2024-12-04 01:17:20 +08:00
10761e92ec BigDecimals:修复 equalsValue 方法第二个参数为 null 报空指针的问题;补充单元测试 2024-12-04 01:16:00 +08:00
65f0bb062c ArrayTools:修复 concat 方法的参数 arrays 中存在 null,从而报空指针的问题;补充单元测试 2024-12-04 01:14:20 +08:00
cca3c8b1ee 修改身份证号的实现 2024-12-02 18:30:38 +08:00
274f2d9874 修改 ParsingFailureException 的注释 2024-12-02 18:29:34 +08:00
2e4e32af45 新增中国二代居民身份证正则常量 2024-12-02 18:29:00 +08:00
c06c486ab9 AssertTools 删除 checkArgumentNotNull 方法,新增 checkExists、checkAffectedRows、checkAffectedOneRow 方法。 2024-12-02 18:28:23 +08:00
d499433a34 新增 DataNotExistsException 2024-12-02 18:26:39 +08:00
1c98d05302 修改 Chinese2ndGenIDCardNumber 2024-11-29 18:24:41 +08:00
7af3b5ad7c 修改正则常量 2024-11-29 18:24:00 +08:00
6bbf55160b 重载新增 Numbers#nullToZero(Byte) 2024-11-27 22:30:51 +08:00
cee89835e5 IdGenerator#toSimpleString 新增参数校验 2024-11-27 21:39:34 +08:00
296e8995d6 去除 toZonedDateTime 的 Deprecated 注解 2024-11-26 16:05:42 +08:00
d2d824462b 新增测试进度文件记录 2024-11-19 23:15:22 +08:00
5dbaba3c5a 新增 TODO 2024-11-19 23:10:54 +08:00
fe72ddb784 更改测试类所在包 2024-11-19 23:10:39 +08:00
86d2716b8d Numbers:重载 BigInteger 的 sum 方法;新增 nullToZero 方法 2024-11-19 22:51:16 +08:00
3b82e27738 ArrayTools:补充重载方法;新增 indexOf、lastIndexOf、contains 方法 2024-11-19 22:49:49 +08:00
fb9cb8ada9 BigDecimals:新增 sum、nullToZero 方法 2024-11-19 22:49:22 +08:00
8d24d8de23 添加注释 2024-11-19 21:59:03 +08:00
3f0c14f2d9 删除代表区间的 Interval,使用 guava 的 Range 即可。 2024-11-19 20:29:33 +08:00
7951172d68 AbstractMapWrapper 覆写 hashCode 和 equals 2024-11-02 11:47:41 +08:00
476d3c5ac9 ArrayTools 和 StringTools 新增 repeat 方法。 2024-11-01 17:41:01 +08:00
9068d409f5 1.0.0-alpha
Signed-off-by: ZhouXY108 <luquanlion@outlook.com>
2024-11-01 17:30:45 +08:00
f2e12a4aab 1.0.x
Signed-off-by: ZhouXY108 <luquanlion@outlook.com>
2024-11-01 17:27:11 +08:00
4a84941bd9 新增 checkNotNull 方法 2024-11-01 10:36:07 +08:00
87c9273e43 补充 .editorconfig 2024-10-25 21:27:14 +08:00
72c55ec0e3 更新 joda-time 和测试依赖的 gson 2024-10-25 09:32:52 +08:00
f4b7005b92 格式化代码。 2024-10-21 23:17:52 +08:00
443116a5a2 新增通用异常,调整异常结构。 2024-10-21 23:17:41 +08:00
5932bbd53f 删除多余的函数式接口 2024-10-21 21:15:35 +08:00
e5cd981508 修改注释 2024-10-21 18:18:53 +08:00
66d0e811f7 新增允许抛出异常的 Executable、ThrowingConsumer、ThrowingSupplier 和 ThrowingPredicate 2024-10-21 01:54:34 +08:00
ab05b55b04 修改方法名 2024-10-21 01:42:01 +08:00
269f9d686e 重构代码 2024-10-21 01:25:08 +08:00
442374e53b ValidatableStringRecord 暂不支持序列化 2024-10-21 00:35:10 +08:00
85939e4fc4 删除 ImmutableObject,使用 com.google.errorprone.annotations.Immutable 2024-10-21 00:26:06 +08:00
fa5b8aba7d 修改 toString 的测试用例 2024-10-20 21:52:26 +08:00
0c3e3f77ce 更新依赖。 2024-10-18 18:41:06 +08:00
3c179f1488 修改正则。 2024-10-18 18:40:55 +08:00
f2298e934c 删除多余方法,修改方法名。 2024-10-18 18:40:41 +08:00
cfc1ddc6b3 新增 InvalidInputException 2024-10-18 18:31:40 +08:00
4ea3a3a6ab 添加注释,添加重载方法。 2024-10-18 18:31:20 +08:00
949c59fc1e 新增 ArrayTools#fill 方法 2024-10-14 16:36:01 +08:00
1ade08783f 优化代码 2024-10-14 16:35:19 +08:00
1b0cf2bd42 更改 YearQuarter 字符串格式。 2024-10-12 02:00:33 +08:00
be0e21022e 补充、修改 Copyright 2024-10-12 00:57:35 +08:00
c968b004e8 更新 Copyright。 2024-10-12 00:27:28 +08:00
ff472384fb 新增 Chinese2ndGenIDCardNumber,由于中国第二代居民身份证号 2024-10-12 00:23:03 +08:00
538ad85124 改进 ValidatableStringRecord,子类可以获取 matcher 做更多操作。 2024-10-12 00:22:02 +08:00
979ff1760f 修改方法名。 2024-10-12 00:21:19 +08:00
6eb5ada623 新增枚举“Gender” 2024-10-11 21:19:28 +08:00
88f96c7116 新增 ArrayTools#isAllElementsNotNull 2024-10-11 17:14:49 +08:00
de0a732616 新增 BigDecimals#toPlainString 2024-10-11 17:11:56 +08:00
b72fd59b46 使用 AssertTools 代替 PreconditionsExt。 2024-10-11 17:11:06 +08:00
304dccc658 添加 StringTools#isNotBlank 2024-10-09 18:46:18 +08:00
5e450a9bdb 修改方法名。 2024-10-09 18:45:26 +08:00
62fd1b900f 修复数组拼接的 bug 2024-09-21 10:10:18 +08:00
424194d67b 忽略 Enumeration 的测试类的 deprecation 警告 2024-09-03 17:08:39 +08:00
007e44c1d2 优化 RegexTools 代码 2024-09-03 17:07:42 +08:00
e9c93a273c 使用 guava chache 缓存 DateTimeFormatter 对象,限制缓存数量。 2024-09-03 15:50:41 +08:00
79aebe4fcc 补充缺失的校验 2024-09-03 15:34:52 +08:00
7a744c8953 废弃 Enumeration 类 2024-09-03 15:26:36 +08:00
322ecf4db8 优化 YearQuarter 的代码 2024-09-02 17:09:36 +08:00
1a9960dbf1 新增区间的建模 2024-09-02 17:08:42 +08:00
5e5202ff3a 删除异常基类。应用系统应按实际需要自定义异常体系。 2024-08-30 14:58:47 +08:00
0b9635880e 优化 PreconditionsExt 测试代码 2024-08-30 14:56:38 +08:00
5c1f6046ed 修改 IdWorker#generateWorkerIdBaseOnMac 的注释 2024-08-30 14:40:45 +08:00
f55ad5ae0c NoAvailableMacFoundException 添加 serialVersionUID 2024-08-30 14:39:51 +08:00
707712b2c0 添加计算方法。 2024-08-28 17:20:54 +08:00
6d76e9d524 错误码统一进行配置,不作为异常的字段。异常通过 getType 标识不同错误类型。 2024-08-27 19:00:38 +08:00
a59cadc537 测试 diff 2024-08-26 18:26:45 +08:00
aebd66d059 修改 TreeBuilder#buildTreeInternal 中的局部变量名,便于理解。 2024-08-26 17:37:30 +08:00
0aaa9331dc 重构代码;单元测试 2024-08-26 17:36:36 +08:00
43e01e5595 YearQuarter 实现 Comparable 和 Serializable 接口;新增 isBefore 和 isAfter 方法。 2024-08-26 10:41:26 +08:00
fd1126be9b 重构部分代码;修改 API 2024-08-26 10:24:07 +08:00
c92f4fbd5c 新增 ImmutableObject 注解。 2024-08-26 10:21:29 +08:00
527b0f0980 添加注释 2024-08-23 18:31:39 +08:00
91c30412de 完善单元测试。 2024-08-20 00:42:38 +08:00
4ac281d65b 补充注释。 2024-08-20 00:42:11 +08:00
c58e799b1e 添加 YearQuarter 和 Quarter 表示季度;修改 DateTimeTools。 2024-08-19 18:31:56 +08:00
71b3b193d1 修改 API,也使 PagingAndSortingQueryParams 支持 Gson。 2024-08-19 18:31:56 +08:00
516c9d9d79 修改类名,调整包结构。 2024-08-19 18:31:56 +08:00
d580c4757e 更新依赖。 2024-08-19 18:31:56 +08:00
566202ce47 添加注释说明。 2024-08-19 18:31:56 +08:00
0850e765c8 ValidatableStringRecord 支持序列化。 2024-08-14 11:50:08 +08:00
92b4c5f3fc 更新 hutool 版本 2024-08-12 11:11:27 +08:00
c7ed383382 优化代码 2024-07-13 20:23:04 +08:00
b86bb6c15b 添加方法,获取季度。 2024-06-04 16:07:08 +08:00
245c31bdce 优化代码。 2024-05-28 09:41:07 +08:00
33b271ddbe 删除 RestfulResult 类 2024-05-28 09:40:51 +08:00
47eea5fc09 优化代码 2024-05-28 09:38:59 +08:00
48bd4f1b31 优化功能 2024-05-28 09:38:37 +08:00
2297536307 添加判空 2024-05-28 09:37:51 +08:00
78ed0c8ed1 使用 Seata 最新的 IdWorker 代码,将生成 WorkerId 的方法添加回来 2024-05-28 09:36:50 +08:00
9ccb12f956 优化代码 2024-05-28 09:35:04 +08:00
d3b19c6cb0 允许构建时进行排序。 2024-04-16 21:06:02 +08:00
22687605b2 调整代码。 2024-04-16 21:05:07 +08:00
b9d4901b67 重构。 2024-04-16 21:04:08 +08:00
d2d760e866 ValidatableStringRecord 的值满足 Pattern 即可,不要求 not blank。 2024-04-16 21:03:24 +08:00
b08562adaf 删除 jackson-annotations 依赖。 2024-04-07 16:45:40 +08:00
292e982ab2 不允许 id 重复。 2024-04-07 16:44:48 +08:00
87c9d15751 修改 Javadoc。 2024-04-07 16:26:11 +08:00
3455dd1f32 UnifiedResponse 不再基于 Map。 2024-04-07 16:24:25 +08:00
9af12f35ca 支持分别按照多个不同列的递增或递减排序。 2024-04-07 16:21:39 +08:00
b2ece2fab5 删除注解。可使用 JavaDoc 标记所覆写的父类方法。 2024-04-07 16:14:03 +08:00
cc34af04c2 统一工具类命名规则。 2024-04-03 16:20:41 +08:00
09b5b1e0f3 添加元注解 @Inherited。 2024-04-03 15:29:20 +08:00
b547642e5e 新增 DefinedIn 注解。 2024-03-30 20:48:08 +08:00
0b968e2c7b 格式化 2024-03-19 08:29:57 +08:00
f0cc49d06c fix bug. 2024-03-19 08:29:47 +08:00
39fe3d30d1 修改方法名 2024-03-15 09:29:30 +08:00
4fa4b41fae 重构代码 2024-03-15 09:29:30 +08:00
3d5a3ddbee 修改、新增方法 2024-03-15 09:29:30 +08:00
9a0b6404cb 添加 equalsCode 方法 2024-03-15 09:29:30 +08:00
7f80a411db 修改类名,与 guava 的 Predicates 区分。 2024-03-14 23:18:44 +08:00
dc9e0d1b53 fix bug. 2024-03-14 23:16:08 +08:00
8d9ccdb08e 删除意义不大的集合工具。 2024-03-06 15:15:48 +08:00
fba176864c 重命名注解 2024-03-06 15:09:25 +08:00
725283c829 新增 Ref 2024-03-06 15:09:25 +08:00
96fb846864 简化代码。 2024-03-06 15:09:25 +08:00
fe190d8f43 删除集合转 Map 的工具方法,使用流即可。 2024-03-06 15:09:24 +08:00
e5c2ba99c3 修改类名。 2024-03-06 15:01:51 +08:00
c472050d00 删除 SynchronizedTable,使用 guava自带的 Tables#synchronizedTable 即可。 2024-03-06 15:01:51 +08:00
1cf0b19ad0 允许 message 和 data 参数传 null。 2024-03-06 14:59:30 +08:00
5d0af2dad5 修改 API,使 TreeBuilder 实例可以复用。 2024-03-06 14:59:30 +08:00
5cee71a342 调整集合相关工具类。 2024-02-07 09:27:42 +08:00
81c5b6972a 更新 guava 版本 2023-11-19 09:46:39 +08:00
6a66b51b8e 修改方法名。 2023-11-19 09:38:53 +08:00
cc10f11b37 完善 Javadoc。 2023-10-30 09:18:27 +08:00
ce62bdcdc6 完善文档注释。 2023-10-30 09:09:32 +08:00
94d34faffd 新增 BaseRuntimeException 作为基础运行时异常,原 BaseException 作为基础检查型异常。 2023-10-18 10:54:12 +08:00
177 changed files with 22571 additions and 5197 deletions

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
root = true
[*]
# 缩进
indent_style = space
indent_size = 4
# 行尾
end_of_line = lf
# 字符集
charset = utf-8
# 删除行尾空格
trim_trailing_whitespace = true
# 文件最后插入空行
insert_final_newline = true

2
.gitignore vendored
View File

@@ -3,6 +3,8 @@ target/
!**/src/main/**/target/
!**/src/test/**/target/
.flattened-pom.xml
### IntelliJ IDEA ###
.idea
*.iws

64
NOTICE Normal file
View File

@@ -0,0 +1,64 @@
Plusone Commons
Copyright 2022-present Zhou Xingyi
This product includes software developed by
Zhou Xingyi (周兴毅) (https://gitea.zhouxy.xyz/plusone).
This product is licensed to you under the Apache License, Version 2.0
(the "License"). You may not use this product except in compliance with
the License.
===========================================================================
Third-party components and their licenses:
===========================================================================
This software contains code from the following third-party projects:
1. Apache Seata
- Component: IdWorker class implementation
- Source: org.apache.seata.common.util.IdWorker
- Origin: https://github.com/apache/incubator-seata/blob/2.x/common/src/main/java/org/apache/seata/common/util/IdWorker.java
- License: Apache License 2.0
- License URL: https://www.apache.org/licenses/LICENSE-2.0.txt
- Copyright: The Apache Software Foundation
===========================================================================
Dependencies and their licenses:
===========================================================================
The following dependencies are used in this project:
Required Dependencies:
- guava: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
Optional Dependencies:
- gson: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
- jsr305: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
- joda-time: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
Test Dependencies:
- commons-lang3: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
- Logback: Eclipse Public License 1.0 (https://www.eclipse.org/org/documents/epl-1.0/EPL-1.0.txt) / LGPL 2.1 (https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)
- Slf4j: MIT License (https://mit-license.org/)
- JUnit: Eclipse Public License 2.0 (https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt)
- lombok: MIT License (https://mit-license.org/)
- hutool: MulanPSL-2.0 (http://license.coscl.org.cn/MulanPSL2)
- MyBatis: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
- h2: MPL 2.0 (https://www.mozilla.org/en-US/MPL/2.0/) / EPL 1.0 (https://www.eclipse.org/org/documents/epl-1.0/EPL-1.0.txt)
- Jackson: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt)
===========================================================================
Apache License 2.0 Notice:
===========================================================================
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# Plusone Commons
## 1. 简介
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发。
一开始是为了补充日常开发中guava 认为不需要,而我又用得上的工具,所以需要结合 guava 使用。后面也包含了一些从日常工作与学习中抽离出来可以通用的东西。
Plusone Commons 的工具类不追求“大而全”,而是只提供相对需要的部分功能。
> 未来一些不够“通用”的组件会迁移到更合适的模块中。
## 2. 安装
项目基于 OpenJDK 8 和 maven 构建。
使用 Maven 添加依赖:
```xml
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-commons</artifactId>
<version>${plusone-commons.version}</version>
</dependency>
```
## 3. 功能
详细功能说明请查阅文档:
+ [文档地址](/plusone-commons/docs)
## 4. 代码仓库
项目仓库一共建了三个:
+ [GitHub](https://github.com/ZhouXY108/plusone-commons)
+ [gitee](https://gitee.com/zhouxy108/plusone-commons)
+ [自建 Gitea 仓库](https://gitea.zhouxy.xyz/plusone/plusone-commons)
欢迎在 GitHub 和 gitee 上通过 issue 反馈使用过程中发现的问题和建议,也接受善意的 PR。
## 5. 许可
项目使用 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) 开源,相关声明请参阅 `NOTICE` 文件。

52
cspell.json Normal file
View File

@@ -0,0 +1,52 @@
{
"version": "0.2",
"ignorePaths": [
"*/src/test"
],
"dictionaryDefinitions": [],
"dictionaries": [],
"words": [
"aliyun",
"baomidou",
"Batis",
"buildmetadata",
"Consolas",
"cspell",
"databind",
"datasource",
"dbutils",
"fasterxml",
"findbugs",
"gson",
"Hikari",
"hutool",
"jasypt",
"jbcrypt",
"Jdbc",
"joda",
"logback",
"mapstruct",
"mindrot",
"Multimap",
"Multiset",
"mybatis",
"Nonnull",
"NOSONAR",
"okhttp",
"okio",
"ooxml",
"overriden",
"plusone",
"println",
"projectlombok",
"querydsl",
"regexs",
"Seata",
"sonarlint",
"springframework",
"TWEPOCH",
"Validatable",
"zhouxy"
],
"import": []
}

View File

@@ -0,0 +1,8 @@
## 1. 注解
|注解|说明|
|--|--|
| **StaticFactoryMethod** | **标识静态工厂方法***《Effective Java》***Item1** 建议考虑用静态工厂方法替换构造器, 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。|
| **ReaderMethod** / **WriterMethod** | **分别标识读方法(如 getter或写方法如 setter**。<br>*最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。*|
| **UnsupportedOperation** | **标识该方法不被支持或没有实现**,将抛出 `UnsupportedOperationException`。 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。|
| **Virtual** | Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 **Virtual 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写**。|
| **ValueObject** | 标记一个类,表示其作为**值对象**,区别于 Entity。|

View File

@@ -0,0 +1,39 @@
## 2. 集合
### 2.1. CollectionTools
简单的集合工具类,包含判空等常用方法。
### 2.2. MapModifier
Map 修改器。封装一系列对 Map 数据的修改操作,修改 Map 的数据。可以用于 Map 的数据初始化等操作。
```java
// MapModifier
MapModifier<String, Object> modifier = new MapModifier<String, Object>()
.putAll(commonProperties)
.put("username", "Ben")
.put("accountStatus", LOCKED);
// 从 Supplier 中获取 Map并修改数据
Map<String, Object> map = modifier.getAndModify(HashMap::new);
// 可以灵活使用不同 Map 类型的不同构造器
Map<String, Object> map = modifier.getAndModify(() -> new HashMap<>(8));
Map<String, Object> map = modifier.getAndModify(() -> new HashMap<>(anotherMap));
Map<String, Object> map = modifier.getAndModify(TreeMap::new);
Map<String, Object> map = modifier.getAndModify(ConcurrentHashMap::new);
// 修改已有的 Map
modifier.modify(map);
// 创建一个有初始化数据的不可变的 Map
Map<String, Object> map = modifier.getUnmodifiableMap();
// 链式调用创建并初始化数据
Map<String, Object> map = new MapModifier<String, Object>()
.putAll(commonProperties)
.put("username", "Ben")
.put("accountStatus", LOCKED)
.getAndModify(HashMap::new);
```

View File

@@ -0,0 +1,118 @@
## 3. 异常
### 3.1. 业务异常
|异常|描述|
|---|---|
|`BizException`|**业务异常**<br>*用户可继承 `BizException` 自定义业务异常。*|
`RequestParamsException`|**用户请求参数错误**|
|» » `InvalidInputException`|**用户输入内容非法**<br>00 - **DEFAULT** (用户输入内容非法)<br>01 - **CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS** (包含非法恶意跳转链接)<br>02 - **CONTAINS_ILLEGAL_WORDS** (包含违禁敏感词)<br>03 - **PICTURE_CONTAINS_ILLEGAL_INFORMATION** (图片包含违禁信息)<br>04 - **INFRINGE_COPYRIGHT** (文件侵犯版权)|
### 3.2. 系统异常
|异常|描述|
|---|---|
|`SysException`|**系统异常**(表示技术异常)<br>*用户可继承 `SysException` 自定义系统异常。*|
`DataOperationResultException`|**数据操作的结果不符合预期**|
`NoAvailableMacFoundException`|**无法找到可访问的 Mac 地址**|
### 3.3. 其它异常
|异常|描述|
|---|---|
|`DataNotExistsException`|**数据不存在异常**|
|`ParsingFailureException`|**数据解析异常**<br>00 - **DEFAULT** (解析失败)<br>10 - **NUMBER_PARSING_FAILURE** (数字转换失败)<br>20 - **DATE_TIME_PARSING_FAILURE** (时间解析失败)<br>30 - **JSON_PARSING_FAILURE** (JSON 解析失败)<br>40 - **XML_PARSING_FAILURE** (XML 解析失败)|
### 3.4. 多类型异常
异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。
异常实现 `IMultiTypesException``getType` 方法,返回对应的场景类型。
枚举实现 `IExceptionType` 接口,表示不同的异常场景。也可以实现 `IExceptionFactory`,用于创建对应场景的异常。
```java
public final class LoginException
extends RuntimeException
implements IMultiTypesException<LoginException.Type> {
private static final long serialVersionUID = 881293090625085616L;
private final Type type;
private LoginException(@Nonnull Type type, @Nonnull String message) {
super(message);
this.type = type;
}
private LoginException(@Nonnull Type type, @Nonnull Throwable cause) {
super(cause);
this.type = type;
}
private LoginException(@Nonnull Type type,
@Nonnull String message,
@Nonnull Throwable cause) {
super(message, cause);
this.type = type;
}
@Override
public @Nonnull Type getType() {
return this.type;
}
// ...
public enum Type implements IExceptionType<String>, IExceptionFactory<LoginException> {
DEFAULT("00", "当前会话未登录"),
NOT_TOKEN("10", "未提供token"),
INVALID_TOKEN("20", "token无效"),
TOKEN_TIMEOUT("30", "token已过期"),
BE_REPLACED("40", "token已被顶下线"),
KICK_OUT("50", "token已被踢下线"),
;
@Nonnull
private final String code;
@Nonnull
private final String defaultMessage;
Type(@Nonnull String code, @Nonnull String defaultMessage) {
this.code = code;
this.defaultMessage = defaultMessage;
}
@Override
public @Nonnull String getCode() {
return code;
}
@Override
public @Nonnull String getDefaultMessage() {
return defaultMessage;
}
@Override
public @Nonnull LoginException create() {
return new LoginException(this, this.defaultMessage);
}
@Override
public @Nonnull LoginException create(@Nonnull String message) {
return new LoginException(this, message);
}
@Override
public @Nonnull LoginException create(@Nonnull Throwable cause) {
return new LoginException(this, cause);
}
@Override
public @Nonnull LoginException create(@Nonnull String message, @Nonnull Throwable cause) {
return new LoginException(this, message, cause);
}
}
}
```
使用时,可以使用这种方式创建并抛出异常:
```java
throw LoginException.Type.TOKEN_TIMEOUT.create();
```

View File

@@ -0,0 +1,22 @@
## 4. - 函数式编程
### 4.1. PredicateTools
`PredicateTools` 用于 `Predicate` 的相关操作。
### 4.2. Functional interfaces
补充可能用得上的函数式接口:
| Group | FunctionalInterface | method |
| --- | --- | --- |
| UnaryOperator | **BoolUnaryOperator** | boolean applyAsBool (boolean) |
| UnaryOperator | **CharUnaryOperator** | char applyAsChar(char) |
| Throwing | **Executable** | void execute() throws E |
| Throwing | **ThrowingConsumer** | void accept(T) throws E |
| Throwing | **ThrowingFunction** | R apply(T) throws E |
| Throwing | **ThrowingPredicate** | boolean test(T) throws E |
| Throwing | **ThrowingSupplier** | T get() throws E |
| Optional | **OptionalSupplier** | Optional<T> get() throws E |
| Optional | **ToOptionalBiFunction** | Optional<R> apply(T,U) |
| Optional | **ToOptionalFunction** | Optional<R> apply(T) |

View File

@@ -0,0 +1,100 @@
## 5. 数据模型
### 5.1. 业务模型
| | 类型 | 描述 |
| --- | --- | --- |
| 接口 | `IDCardNumber` | 身份证号 |
| 值对象 | » `Chinese2ndGenIDCardNumber` | 中国二代居民身份证号 |
| 值对象 | `SemVer` | 语义化版本号 |
| 值对象(枚举)| `Gender` | 性别 |
| ~~值对象(抽象类)~~| ~~`ValidatableStringRecord`~~ | ~~带校验的字符串值对象~~ |
### 5.2. 数据传输对象
#### 5.2.1. 分页查询
`PagingAndSortingQueryParams` (分页排序查询参数)
`PagingAndSortingQueryParams` 包含三个主要的属性:
- **size** - 每页显示的记录数
- **pageNum** - 当前页码
- **orderBy** - 排序条件
分页必须伴随着排序,不然可能出现同一个对象重复出现在不同页,有的对象不被查询到的情况。
其中 `orderBy` 是一个 `List<String>`,可以指定多个排序条件,每个排序条件是一个字符串, 格式为“**属性名-ASC**”或“**属性名-DESC**”,分别表示升序和降序。
例如当 `orderBy` 的值为 `["name-ASC","age-DESC"]`,意味着要按 `name` 进行升序排列,`name` 相同的情况下则按 `age` 进行降序排列。
用户可继承 `PagingAndSortingQueryParams` 构建自己的分页查询入参,子类需在构造器中调用 `PagingAndSortingQueryParams` 的构造器,传入一个 `PagingParamsBuilder` 用于构建分页参数。同一场景下,复用一个 {@link PagingParamsBuilder} 实例即可。
构建 `PagingParamsBuilder` 时,需传入一个 `Map` 作为可排序字段的白名单,`key` 是供前端指定用于排序的**属性名**`value` 是对应数据库中的**字段名**,只有在白名单中指定的属性名才允许作为排序条件。
```java
@ToString(callSuper = true)
class AccountQueryParams extends PagingAndSortingQueryParams {
private static final Map<String, String> PROPERTY_COLUMN_MAP = ImmutableMap.<String, String>builder()
.put("id", "id")
.put("username", "username")
.build();
private static final PagingParamsBuilder PAGING_PARAMS_BUILDER = PagingAndSortingQueryParams
.pagingParamsBuilder(20, 100, PROPERTY_COLUMN_MAP);
public AccountQueryParams() {
super(PAGING_PARAMS_BUILDER);
}
private @Getter @Setter Long id;
private @Getter @Setter String username;
private @Getter @Setter String email;
private @Getter @Setter Integer status;
}
```
使用时调用 `PagingAndSortingQueryParams#buildPagingParams()` 方法获取分页参数 `PagingParams`。分页结果可以存放到 `PageResult` 中,作为出参。
```java
public PageResult<AccountVO> queryPage(AccountQueryParams params) {
// 获取分页参数
PagingParams pagingParams = params.buildPagingParams();
// 从 params 获取字段查询条件,从 pagingParams 获取分页条件,查询一页数据
List<AccountVO> list = accountQueries.queryAccountList(params, pagingParams);
// 查询总记录数
long count = accountQueries.countAccount(params);
// 返回分页结果
return PageResult.of(list, count);
}
```
#### 5.2.2. UnifiedResponse
`UnifiedResponse` 对返回给前端的数据进行封装,包含 `code``message``data`
`UnifiedResponses``UnifiedResponse` 的工厂类。用于快速构建 `UnifiedResponse` 对象,默认的成功代码为 `2000000`
用户可以继承 `UnifiedResponses` 实现自己的工厂类,自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG以及工厂方法。如下所示
```java
// 自定义工厂类
public static class CustomUnifiedResponses extends UnifiedResponses {
public static final String SUCCESS_CODE = "000";
public static final String DEFAULT_SUCCESS_MSG = "成功";
public static <T> UnifiedResponse<T> success() {
return of(SUCCESS_CODE, DEFAULT_SUCCESS_MSG);
}
public static <T> UnifiedResponse<T> success(@Nullable String message) {
return of(SUCCESS_CODE, message);
}
public static <T> UnifiedResponse<T> success(@Nullable String message, @Nullable T data) {
return of(SUCCESS_CODE, message, data);
}
private CustomUnifiedResponses() {
super();
}
}
// 使用自定义工厂类
CustomUnifiedResponses.success("查询成功", userList); // 状态码为 000
```
> 见 [issue#22 @Gitea](http://gitea.zhouxy.xyz/plusone/plusone-commons/issues/22)

View File

@@ -0,0 +1,24 @@
## 6. 时间 API
### 6.1. 季度
模仿 JDK 的 `java.time.Month``java.time.YearMonth` 实现 `xyz.zhouxy.plusone.commons.time.Quarter``xyz.zhouxy.plusone.commons.timeYearQuarter`,对季度进行建模。
*这两个类的代码修改后,也提交给了 **hutool**。见 gitee
上的 [pr#1324](https://gitee.com/chinabugotech/hutool/pulls/1324)*。
### 6.2. DateTimeTools
`xyz.zhouxy.plusone.commons.util.DateTimeTools` 提供了包含 Java 旧的时间 API 和 `java.time` API 在内的日期时间的常用操作。
### 6.3. JodaTimeTools
`xyz.zhouxy.plusone.commons.util.JodaTimeTools` 提供了 JodaTime 和 `java.time` API 相互转换的工具方法:
- `toJodaInstant`
- `toJavaInstant`
- `toJodaDateTime`
- `toZonedDateTime`
- `toJodaLocalDateTime`
- `toJavaLocalDateTime`
- `toJavaZone`
- `toJodaZone`

View File

@@ -0,0 +1,275 @@
## 7. 工具类
### 7.1. 数组工具ArrayTools
| 方法 | 描述 |
| --- | --- |
| `isEmpty` | 判断数组是否为空 |
| `isNotEmpty` | 判断数组是否不为空 |
| `isAllElementsNotNull` | 判断数组中所有元素是否不为空 |
| `concat` | 拼接数组 |
| `repeat` | 重复数组中的元素 |
| `fill` | 填充数组 |
| `indexOf` | 获取元素在数组中的索引 |
| `lastIndexOf` | 获取元素最后出现在数组中的索引 |
| `contains` | 判断数组中是否包含某个元素 |
### 7.2. 断言工具AssertTools
`AssertTools` 不封装过多判断逻辑,鼓励充分使用项目中的工具类对数据进行判断:
```java
AssertTools.checkArgument(StringUtils.hasText(str), "The argument cannot be blank.");
AssertTools.checkState(ArrayUtils.isNotEmpty(result), "The result cannot be empty.");
AssertTools.checkCondition(!CollectionUtils.isEmpty(roles),
() -> new InvalidInputException("The roles cannot be empty."));
AssertTools.checkCondition(RegexTools.matches(email, PatternConsts.EMAIL),
"must be a well-formed email address");
```
### 7.3. 枚举工具
#### ~~7.3.1 枚举类Enumeration已废弃~~
~~`Enumeration` 的实现来自于 .net 社区。因为 C# 本身的枚举不带行为,所以继承自 `Enumeration` 类,以实现带行为的枚举常量。~~
**~~但 Java 的枚举可以带行为,故大多数情况下不需要这种设计。~~**
#### 7.3.2 Enum 工具类EnumTools
用于枚举的 ordinal 和枚举值的转换等操作。
由于不推荐使用枚举的 ordinal**故大多数方法已废弃**。更推荐的实现是枚举实现 `IWithCode` 之类的接口,在枚举中提供枚举值和枚举码的转换。
### 7.4. ID 生成器
#### 7.4.1. ID 生成器IdGenerator
- 提供了 `UUID` 相关的方法
| 方法 | 描述 |
| --- | --- |
| newUuid | 获取新的 `UUID` |
| uuidString | 获取新的 UUID 字符串 |
| simpleUuidString | 获取新的 UUID 字符串(无连接符) |
| toSimpleString | 将 `UUID` 转换为无连接符的字符串 |
- 使用 `IdWorker` *(来自 **Seata** 的雪花算法的变种)* 生成分布式唯一 ID
#### 7.4.2. IdWorker
来自 [Apache Seata](https://seata.apache.org/) 的 [`org.apache.seata.common.util.IdWorker`](https://github.com/apache/incubator-seata/blob/2.x/common/src/main/java/org/apache/seata/common/util/IdWorker.java),是雪花算法的变种。
详细介绍参考以下文章:
- [Seata基于改良版雪花算法的分布式UUID生成器分析](https://seata.apache.org/zh-cn/blog/seata-analysis-UUID-generator)
- [关于新版雪花算法的答疑](https://seata.apache.org/zh-cn/blog/seata-snowflake-explain)
- [在开源项目中看到一个改良版的雪花算法,现在它是你的了。](https://juejin.cn/post/7264387737276203065)
- [关于若干读者,阅读“改良版雪花算法”后提出的几个共性问题的回复。](https://juejin.cn/post/7265516484029743138)
#### 7.4.3. SnowflakeIdGenerator
`SnowflakeIdGenerator` 是原版的雪花算法的实现
### 7.5. 树构建器TreeBuilder
`TreeBuilder` 是一个树构建器,用于将列表数据构建为树结构。
`TreeBuilder` 构造器的入参:
- **identityGetter**: 从节点中获取其标识的逻辑
- **parentIdentityGetter**: 获取父节点标识的逻辑
- **addChild**: 添加子节点的逻辑
- **defaultComparator**: 默认的 Comparator用于排序
> **注意:`TreeBuilder` 的 `buildTree` 方法,会直接更改列表中的节点。设计初衷是将查询到的列表,构建成为树结构之后直接返回给前端,如果需要,请在调用之前做深拷贝,然后再将深拷贝的结果作为入参传入。**
以下示例演示 `TreeBuilder` 的使用:
#### 7.5.1. 处理相对复杂的 entity
假设有如下的实体类:
```java
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
@ToString
class Menu implements Serializable {
protected final @Getter String parentMenuCode;
protected final @Getter String menuCode;
protected final @Getter String title;
protected final @Getter int orderNum;
private static final long serialVersionUID = 20240917181424L;
}
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
class MenuItem extends Menu {
private final @Getter String url;
private MenuItem(String parentMenuCode, String menuCode, String title, String url, int orderNum) {
super(parentMenuCode, menuCode, title, orderNum);
this.url = url;
}
static MenuItem of(String parentMenuCode, String menuCode, String title, String url, int orderNum) {
return new MenuItem(parentMenuCode, menuCode, title, url, orderNum);
}
static MenuItem of(String menuCode, String title, String url, int orderNum) {
return new MenuItem(null, menuCode, title, url, orderNum);
}
private static final long serialVersionUID = 20240917181910L;
}
@ToString(callSuper = true)
class MenuList extends Menu {
private List<Menu> children;
private MenuList(String parentMenuCode, String menuCode, String title, int orderNum) {
super(parentMenuCode, menuCode, title, orderNum);
}
static MenuList of(String parentMenuCode, String menuCode, String title, int orderNum) {
return new MenuList(parentMenuCode, menuCode, title, orderNum);
}
static MenuList of(String menuCode, String title, int orderNum) {
return new MenuList(null, menuCode, title, orderNum);
}
@SuppressWarnings("unused")
static MenuList of(String menuCode, String title, Iterable<Menu> children, int orderNum) {
return of(null, menuCode, title, children, orderNum);
}
static MenuList of(String parentMenuCode, String menuCode, String title, Iterable<Menu> children,
int orderNum) {
final MenuList instance = of(parentMenuCode, menuCode, title, orderNum);
children.forEach(instance::addChild);
return instance;
}
public void addChild(Menu child) {
if (this.children == null) {
this.children = Lists.newArrayList();
}
this.children.add(child);
}
private static final long serialVersionUID = 20240917181917L;
}
```
其中,`Menu` 表示菜单节点,其子类 `MenuItem` 表示菜单项,在树中作为叶子节点,另一子类 `MenuList` 表示菜单列表,其子菜单放在 `children` 字段中。`MenuList` 提供了 `addChild` 方法用于将子菜单添加到 `children` 中。
使用以下方式构建并使用 `TreeBuilder`
```java
// 创建 TreeBuilder
TreeBuilder<Menu, MenuList, String> treeBuilder = new TreeBuilder<>(
// getMenuCode 方法获取节点标识
Menu::getMenuCode,
// getParentMenuCode 方法获取父节点标识,如果父节点不存在,返回 Optional.empty()
menu -> Optional.ofNullable(menu.getParentMenuCode()),
// addChild 方法用于将子节点添加到父节点的 children 中
MenuList::addChild,
// 默认的 Comparator使用 orderNum 进行排序
Comparator.comparing(Menu::getOrderNum));
// 需要的话进行深拷贝
List<Menu> clonedMenus = menus.stream().map(ObjectUtil::clone).collect(Collectors.toList());
// 按照创建时设置的逻辑,构建树结构
List<Menu> result = treeBuilder.buildTree(clonedMenus);
```
#### 7.5.2. 处理 POJO
`TreeBuilder` 也可以处理 POJO只需要自定义 `TreeBuilder` 所需的入参即可。
```java
// POJO
@Data
class Menu implements Serializable {
private final String parentMenuCode;
private final String menuCode;
private final String title;
private final int orderNum;
private final String url;
private List<Menu> children;
private static final long serialVersionUID = 1298482252210272617L;
}
```
使用以下方式构建并使用 `TreeBuilder`
```java
// 创建 TreeBuilder
TreeBuilder<Menu, MenuList, String> treeBuilder = new TreeBuilder<>(
// getMenuCode 方法获取节点标识
Menu::getMenuCode,
// getParentMenuCode 方法获取父节点标识,如果父节点不存在,返回 Optional.empty()
menu -> Optional.ofNullable(menu.getParentMenuCode()),
// 自定义 addChild 逻辑
(menuList, child) -> {
List<Menu> children = menuList.getChildren();
if (children == null) {
children = Lists.newArrayList();
menuList.setChildren(children);
}
children.add(child);
},
// 默认的 Comparator使用 orderNum 进行排序
Comparator.comparing(Menu::getOrderNum));
// 按照创建时设置的逻辑,构建树结构
List<Menu> result = treeBuilder.buildTree(clonedMenus);
```
### 7.6. Ref
`Ref` 包装了一个值,表示对该值的应用。
C# 中允许通过 ref 参数修饰符,将值返回给调用端:
```csharp
void Method(ref int refArgument)
{
refArgument = refArgument + 44;
}
int number = 1;
Method(ref number);
Console.WriteLine(number); // Output: 45
```
`Ref` 使 Java 可以达到类似的效果,如:
```java
void method(Ref<Integer> refArgument) {
refArgument.transformValue(i -> i + 44);
}
Ref<Integer> number = Ref.of(1);
method(number);
System.out.println(number.getValue()); // Output: 45
```
当一个方法需要产生多个结果时,无法有多个返回值,可以使用 `Ref` 作为参数传入,方法内部修改 `Ref` 的值。 调用方在调用方法之后,使用 `getValue()` 获取结果。
```java
String method(Ref<Integer> intRefArgument, Ref<String> strRefArgument) {
intRefArgument.transformValue(i -> i + 44);
strRefArgument.setValue("Hello " + strRefArgument.getValue());
return "Return string";
}
Ref<Integer> number = Ref.of(1);
Ref<String> str = Ref.of("Java");
String result = method(number, str);
System.out.println(number.getValue()); // Output: 45
System.out.println(str.getValue()); // Output: Hello Java
System.out.println(result); // Output: Return string
```
### 7.7 其它工具类
- **`BigDecimals`**: BigDecimal 工具
- **`Numbers`**: 数字工具
- **`OptionalTools`**: Optional 工具
- **`RandomTools`**: 随机工具
- **`RegexTools`**: 正则工具
- **`StringTools`**: 字符串工具
- **`ZipTools`**: zip 工具

View File

@@ -0,0 +1,9 @@
## 8. 其它内容
### 8.1. IWithCode
对于类似枚举这样的类型,通常需要设置固定的码值表示对应的含义。 可实现 `IWithCode``IWithIntCode``IWithLongCode`,便于在需要的地方对这些接口的实现进行处理。
### 8.2. 正则常量
`RegexConsts` 包含常见正则表达式;`PatternConsts` 包含对应的 `Pattern` 对象

135
plusone-commons/pom.xml Normal file
View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-parent</artifactId>
<version>1.1.0-SNAPSHOT</version>
</parent>
<artifactId>plusone-commons</artifactId>
<name>Plusone Commons</name>
<description>
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发。结合 guava 使用。
</description>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-dependencies</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- ========== Compile Dependencies ========== -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<optional>true</optional>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<optional>true</optional>
</dependency>
<!-- ========== Test Dependencies ========== -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,22 +14,24 @@
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.function;
package xyz.zhouxy.plusone.commons.annotation;
import java.util.OptionalDouble;
import java.util.function.Supplier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* OptionalDoubleSupplier
* ReaderMethod
*
* <p>
* 返回 {@link OptionalDouble} 对象
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @see OptionalDouble
* @see Supplier
* 标识方法是读方法 getter
*
* @author ZhouXY
* @since 1.0.0
* @see WriterMethod
*/
@FunctionalInterface
public interface OptionalDoubleSupplier extends Supplier<OptionalDouble> {
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReaderMethod {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,8 +26,8 @@ import java.lang.annotation.Target;
*
* <p>标识方法为静态工厂方法
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @author ZhouXY
* @since 1.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.Documented;
/**
* UnsupportedOperation
*
* <p>标识方法为不支持的操作。该方法将抛出 {@link UnsupportedOperationException}。
*
* @author ZhouXY
* @version 1.0
* @since 1.0.0
* @see UnsupportedOperationException
*/
@Documented
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface UnsupportedOperation {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package xyz.zhouxy.plusone.commons.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -24,9 +25,10 @@ import java.lang.annotation.Target;
/**
* ValueObject - 值对象
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @author ZhouXY
* @since 1.0.0
*/
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValueObject {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,13 +22,13 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标识该方法是可覆写的
* 标识该方法是虚方法
* <p>该注解用于提醒强调父类虽然有默认实现但子类可以根据自己的需要覆写</p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
*
* @author ZhouXY
* @since 1.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Overridable {
public @interface Virtual {
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* WriterMethod
*
* <p>
* 标识方法是写方法,如 setter。
*
* @author ZhouXY
* @since 1.0.0
* @see ReaderMethod
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WriterMethod {
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 注解
*
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.annotation;

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2022-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.base;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* 规定实现类带有 {@code getCode} 方法。
* 用于像自定义异常等需要带有 {@code code} 字段的类,
* 方便其它地方的程序判断该类的是否实现了此接口,以此获取其实例的 {@code code} 字段的值。
*
* @author ZhouXY
*/
public interface IWithCode<T> {
/**
* 获取码值
* @return 码值
*/
@Nonnull
T getCode();
/**
* 判断 {@code code} 与给定的值是否相等
*
* @param code 用于判断的值
* @return 判断结果
*/
default boolean isCodeEquals(@Nullable T code) {
return Objects.equals(getCode(), code);
}
/**
* 判断是否与给定的 {@link IWithCode} 有着相等的 {@code code}
*
* @param other 用于比较的对象
* @return 判断结果
*/
default boolean isSameCodeAs(@Nullable IWithCode<?> other) {
return other != null && Objects.equals(getCode(), other.getCode());
}
/**
* 判断是否与给定的 {@link IWithIntCode} 有着相等的 {@code code}
*
* @param other 用于比较的对象
* @return 判断结果
*/
default boolean isSameCodeAs(@Nullable IWithIntCode other) {
return other != null && Objects.equals(getCode(), other.getCode());
}
/**
* 判断是否与给定的 {@link IWithLongCode} 有着相等的 {@code code}
*
* @param other 用于比较的对象
* @return 判断结果
*/
default boolean isSameCodeAs(@Nullable IWithLongCode other) {
return other != null && Objects.equals(getCode(), other.getCode());
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2022-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.base;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* 规定实现类带有 {@code getCode} 方法。
* 用于像自定义异常等需要带有 {@code code} 字段的类,
* 方便其它地方的程序判断该类的是否实现了此接口,以此获取其实例的 {@code code} 字段的值。
*
* @author ZhouXY
*/
public interface IWithIntCode {
/**
* 获取码值
* @return 码值
*/
int getCode();
/**
* 判断 {@code code} 与给定的值是否相等
*
* @param code 用于判断的值
* @return 判断结果
*/
default boolean isCodeEquals(int code) {
return getCode() == code;
}
/**
* 判断是否与给定的 {@link IWithCode} 有着相等的 {@code code}
*
* @param other 用于比较的对象
* @return 判断结果
*/
default boolean isSameCodeAs(@Nullable IWithCode<?> other) {
return other != null && Objects.equals(getCode(), other.getCode());
}
/**
* 判断是否与给定的 {@link IWithIntCode} 有着相等的 {@code code}
*
* @param other 用于比较的对象
* @return 判断结果
*/
default boolean isSameCodeAs(@Nullable IWithIntCode other) {
return other != null && getCode() == other.getCode();
}
/**
* 判断是否与给定的 {@link IWithLongCode} 有着相等的 {@code code}
*
* @param other 用于比较的对象
* @return 判断结果
*/
default boolean isSameCodeAs(@Nullable IWithLongCode other) {
return other != null && getCode() == other.getCode();
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2022-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.base;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* 规定实现类带有 {@code getCode} 方法。
* 用于像自定义异常等需要带有 {@code code} 字段的类,
* 方便其它地方的程序判断该类的是否实现了此接口,以此获取其实例的 {@code code} 字段的值。
*
* @author ZhouXY
*/
public interface IWithLongCode {
/**
* 获取码值
* @return 码值
*/
long getCode();
/**
* 判断 {@code code} 与给定的值是否相等
*
* @param code 用于判断的值
* @return 判断结果
*/
default boolean isCodeEquals(long code) {
return getCode() == code;
}
/**
* 判断是否与给定的 {@link IWithCode} 有着相等的 {@code code}
*
* @param other 用于比较的对象
* @return 判断结果
*/
default boolean isSameCodeAs(@Nullable IWithCode<?> other) {
return other != null && Objects.equals(getCode(), other.getCode());
}
/**
* 判断是否与给定的 {@link IWithIntCode} 有着相等的 {@code code}
*
* @param other 用于比较的对象
* @return 判断结果
*/
default boolean isSameCodeAs(@Nullable IWithIntCode other) {
return other != null && getCode() == other.getCode();
}
/**
* 判断是否与给定的 {@link IWithLongCode} 有着相等的 {@code code}
*
* @param other 用于比较的对象
* @return 判断结果
*/
default boolean isSameCodeAs(@Nullable IWithLongCode other) {
return other != null && getCode() == other.getCode();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,15 @@
* limitations under the License.
*/
/**
* 基础内容
*
* @author ZhouXY
*/
@CheckReturnValue
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.base;
/**
* 规定实现类带有 {@code getCode} 方法
* 用于像自定义异常等需要带有 {@code code} 字段的类
* 方便其它地方的程序判断该类的是否实现了此接口以此获取其实例的 {@code code} 字段的值
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
*/
public interface IWithIntCode {
int getCode();
}
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.CheckReturnValue;

View File

@@ -0,0 +1,228 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.collection;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.collect.RangeSet;
import com.google.common.collect.Table;
/**
* 集合工具类
*
* @author ZhouXY
* @since 1.0.0
*/
public class CollectionTools {
// ================================
// #region - isEmpty
// ================================
/**
* 判断集合是否为空
*
* @param collection 集合
* @return 是否为空
*/
public static boolean isEmpty(@Nullable Collection<?> collection) {
return collection == null || collection.isEmpty();
}
/**
* 判断集合是否为空
*
* @param map 集合
* @return 是否为空
*/
public static boolean isEmpty(@Nullable Map<?, ?> map) {
return map == null || map.isEmpty();
}
/**
* 判断集合是否为空
*
* @param table 集合
* @return 是否为空
*/
public static boolean isEmpty(@Nullable Table<?, ?, ?> table) {
return table == null || table.isEmpty();
}
/**
* 判断集合是否为空
*
* @param map 集合
* @return 是否为空
*/
public static boolean isEmpty(@Nullable Multimap<?, ?> map) {
return map == null || map.isEmpty();
}
/**
* 判断集合是否为空
*
* @param set 集合
* @return 是否为空
*/
public static boolean isEmpty(@Nullable Multiset<?> set) {
return set == null || set.isEmpty();
}
/**
* 判断集合是否为空
*
* @param set 集合
* @return 是否为空
*/
public static boolean isEmpty(@Nullable RangeSet<?> set) {
return set == null || set.isEmpty();
}
// ================================
// #endregion - isEmpty
// ================================
// ================================
// #region - isNotEmpty
// ================================
/**
* 判断集合是否不为空
*
* @param collection 集合
* @return 是否不为空
*/
public static boolean isNotEmpty(@Nullable Collection<?> collection) {
return collection != null && !collection.isEmpty();
}
/**
* 判断集合是否不为空
*
* @param map 集合
* @return 是否不为空
*/
public static boolean isNotEmpty(@Nullable Map<?, ?> map) {
return map != null && !map.isEmpty();
}
/**
* 判断集合是否不为空
*
* @param table 集合
* @return 是否不为空
*/
public static boolean isNotEmpty(@Nullable Table<?, ?, ?> table) {
return table != null && !table.isEmpty();
}
/**
* 判断集合是否不为空
*
* @param map 集合
* @return 是否不为空
*/
public static boolean isNotEmpty(@Nullable Multimap<?, ?> map) {
return map != null && !map.isEmpty();
}
/**
* 判断集合是否不为空
*
* @param set 集合
* @return 是否不为空
*/
public static boolean isNotEmpty(@Nullable Multiset<?> set) {
return set != null && !set.isEmpty();
}
/**
* 判断集合是否不为空
*
* @param set 集合
* @return 是否不为空
*/
public static boolean isNotEmpty(@Nullable RangeSet<?> set) {
return set != null && !set.isEmpty();
}
// ================================
// #endregion - isNotEmpty
// ================================
// ================================
// #region - nullToEmpty
// ================================
/**
* 将 {@code null} 转为空 {@code List}
*
* @param <T> List 元素的类型
* @param list list
* @return 如果 {@code list} 为 {@code null},返回空列表;
* 如果 {@code list} 不为 {@code null},返回 {@code list} 本身
*/
@Nonnull
public static <T> List<T> nullToEmptyList(@Nullable List<T> list) {
return list == null ? Collections.emptyList() : list;
}
/**
* 将 {@code null} 转为空 {@code Set}
*
* @param <T> Set 元素的类型
* @param set set
* @return 如果 {@code set} 为 {@code null},返回空集合;
* 如果 {@code set} 不为 {@code null},返回 {@code set} 本身
*/
@Nonnull
public static <T> Set<T> nullToEmptySet(@Nullable Set<T> set) {
return set == null ? Collections.emptySet() : set;
}
/**
* 将 {@code null} 转为空 {@code Map}
*
* @param <K> Map 的键的类型
* @param <V> Map 的值的类型
* @param map map
* @return 如果 {@code map} 为 {@code null},返回空集合;
* 如果 {@code map} 不为 {@code null},返回 {@code map} 本身
*/
@Nonnull
public static <K, V> Map<K, V> nullToEmptyMap(@Nullable Map<K, V> map) {
return map == null ? Collections.emptyMap() : map;
}
// ================================
// #endregion - nullToEmpty
// ================================
private CollectionTools() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,302 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.collection;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import com.google.common.annotations.Beta;
/**
* Map 修改器
*
* <p>
* 封装一系列对 Map 数据的修改操作,修改 Map 的数据。可以用于 Map 的数据初始化等操作。
*
* <pre>
* // MapModifier
* MapModifier&lt;String, Object&gt; modifier = new MapModifier&lt;String, Object&gt;()
* .putAll(commonProperties)
* .put("username", "Ben")
* .put("accountStatus", LOCKED);
*
* // 从 Supplier 中获取 Map并修改数据
* Map&lt;String, Object&gt; map = modifier.getAndModify(HashMap::new);
*
* // 可以灵活使用不同 Map 类型的不同构造器
* Map&lt;String, Object&gt; map = modifier.getAndModify(() -&gt; new HashMap&lt;&gt;(8));
* Map&lt;String, Object&gt; map = modifier.getAndModify(() -&gt; new HashMap&lt;&gt;(anotherMap));
* Map&lt;String, Object&gt; map = modifier.getAndModify(TreeMap::new);
* Map&lt;String, Object&gt; map = modifier.getAndModify(ConcurrentHashMap::new);
*
* // 修改已有的 Map
* modifier.modify(map);
*
* // 创建一个有初始化数据的不可变的 Map
* Map&lt;String, Object&gt; map = modifier.getUnmodifiableMap();
*
* // 链式调用创建并初始化数据
* Map&lt;String, Object&gt; map = new MapModifier&lt;String, Object&gt;()
* .putAll(commonProperties)
* .put("username", "Ben")
* .put("accountStatus", LOCKED)
* .getAndModify(HashMap::new);
* </pre>
*
* @author ZhouXY
* @since 1.1.0
*/
@Beta
public class MapModifier<K, V> {
private final List<Consumer<Map<K, V>>> operations;
/**
* 创建一个空的 MapModifier
*/
public MapModifier() {
this.operations = new ArrayList<>();
}
public MapModifier(int initialCapacity) {
this.operations = new ArrayList<>(initialCapacity);
}
/**
* 添加一个键值对。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要添加的 {@code key}
* @param value 要添加的 {@code value}
* @return MapModifier
*/
public MapModifier<K, V> put(@Nullable K key, @Nullable V value) {
return addOperationInternal(map -> map.put(key, value));
}
/**
* 添加一个键值对,如果 key 已经存在,则不添加。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要添加的 {@code key}
* @param value 要添加的 {@code value}
* @return MapModifier
*/
public MapModifier<K, V> putIfAbsent(@Nullable K key, @Nullable V value) {
return addOperationInternal(map -> map.putIfAbsent(key, value));
}
/**
* 添加多个键值对。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param otherMap 要添加的键值对集合。
* 如果为 {@code null},则什么都不做。
*
* @return MapModifier
*/
public MapModifier<K, V> putAll(@Nullable Map<? extends K, ? extends V> otherMap) {
if (otherMap == null || otherMap.isEmpty()) {
return this;
}
return addOperationInternal(map -> map.putAll(otherMap));
}
/**
* 当 {@code key} 不存在时,计算对应的值,并添加到 {@code map} 中。
*
* <p>
* 调用 {@link Map#computeIfAbsent(Object, Function)}。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要添加的 {@code key}
* @param mappingFunction 计算 {@code key} 对应的值
* @return MapModifier
*/
public MapModifier<K, V> computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
checkArgumentNotNull(mappingFunction, "Mapping function cannot be null.");
return addOperationInternal(map -> map.computeIfAbsent(key, mappingFunction));
}
/**
* 当 {@code key} 存在时,计算对应的值,并添加到 {@code map} 中。
*
* <p>
* 调用 {@link Map#computeIfPresent(Object, BiFunction)}。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要添加的 {@code key}
* @param remappingFunction 计算 {@code key} 对应的值
* @return MapModifier
*/
public MapModifier<K, V> computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
checkArgumentNotNull(remappingFunction, "Remapping function cannot be null.");
return addOperationInternal(map -> map.computeIfPresent(key, remappingFunction));
}
/**
* 删除 {@code key}。
*
* <p>
* <b>注意key 是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param key 要删除的 {@code key}
* @return MapModifier
*/
public MapModifier<K, V> remove(K key) {
return addOperationInternal(map -> map.remove(key));
}
/**
* 清空 {@code map}
*
* @return MapModifier
*/
public MapModifier<K, V> clear() {
return addOperationInternal(Map::clear);
}
/**
* 修改 {@code map}
*
* @param map 要修改的 {@code map}
* @param <T> {@code map} 的类型
*/
public <T extends Map<K, V>> void modify(@Nullable T map) {
if (map == null || this.operations.isEmpty()) {
return;
}
this.operations.forEach(operator -> operator.accept(map));
}
/**
* 修改 {@code map}
*
* @param mapSupplier {@code map} 的 {@link Supplier}
* @param <T> {@code map} 的类型
* @return 修改后的 {@code map}。
* 当从 {@code mapSupplier} 获取的 {@code map} 为 {@code null} 时,返回 {@code null}。
*/
@CheckForNull
public <T extends Map<K, V>> T getAndModify(Supplier<T> mapSupplier) {
checkArgumentNotNull(mapSupplier, "The map supplier cannot be null.");
T map = mapSupplier.get();
modify(map);
return map;
}
/**
* 获取 {@code HashMap}
*
* @return {@code HashMap}
*/
public Map<K, V> getHashMap() {
return getAndModify(HashMap::new);
}
/**
* 获取 {@code LinkedHashMap}
*
* @return {@code LinkedHashMap}
*/
public Map<K, V> getLinkedHashMap() {
return getAndModify(LinkedHashMap::new);
}
/**
* 获取 {@code TreeMap}
*
* @return {@code TreeMap}
*/
public Map<K, V> getTreeMap() {
return getAndModify(TreeMap::new);
}
/**
* 获取 {@code ConcurrentHashMap}
*
* @return {@code ConcurrentHashMap}
*/
public Map<K, V> getConcurrentHashMap() {
return getAndModify(ConcurrentHashMap::new);
}
/**
* 创建一个有初始化数据的不可变的 {@code Map}
*
* @return 不可变的 {@code Map}
*/
public Map<K, V> getUnmodifiableMap() {
return Collections.unmodifiableMap(Objects.requireNonNull(getAndModify(HashMap::new)));
}
private MapModifier<K, V> addOperationInternal(Consumer<Map<K, V>> operator) {
this.operations.add(operator);
return this;
}
/**
* 获取操作数量
*
* @return 操作数量
*/
public int getOperatorCount() {
return this.operations.size();
}
/**
* 是否有操作
*
* @return 如果有操作,则返回 {@code true},否则返回 {@code false}
*/
public boolean hasOperations() {
return !this.operations.isEmpty();
}
@Override
public String toString() {
return "MapModifier [OperatorCount=" + this.operations.size() + "]";
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,12 @@
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.base;
/**
* 规定实现类带有 {@code getCode} 方法
* 用于像自定义异常等需要带有 {@code code} 字段的类
* 方便其它地方的程序判断该类的是否实现了此接口以此获取其实例的 {@code code} 字段的值
* 集合相关工具
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @author ZhouXY
*/
public interface IWithLongCode {
long getCode();
}
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.collection;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,24 +21,77 @@ import java.util.regex.Pattern;
/**
* 正则表达式常量
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @author ZhouXY
* @see RegexConsts
* @see xyz.zhouxy.plusone.commons.util.RegexTools
*/
public final class PatternConsts {
public static final Pattern DATE = Pattern.compile(RegexConsts.DATE);
/**
* yyyyMMdd
*
* @see RegexConsts#BASIC_ISO_DATE
*
*/
public static final Pattern BASIC_ISO_DATE = Pattern.compile(RegexConsts.BASIC_ISO_DATE);
/**
* yyyy-MM-dd
*
* @see RegexConsts#ISO_LOCAL_DATE
*/
public static final Pattern ISO_LOCAL_DATE = Pattern.compile(RegexConsts.ISO_LOCAL_DATE);
/**
* 密码
*
* @see RegexConsts#PASSWORD
*/
public static final Pattern PASSWORD = Pattern.compile(RegexConsts.PASSWORD);
/**
* 验证码
*
* @see RegexConsts#CAPTCHA
*/
public static final Pattern CAPTCHA = Pattern.compile(RegexConsts.CAPTCHA);
public static final Pattern EMAIL = Pattern.compile(RegexConsts.EMAIL);
/**
* 邮箱地址
*
* @see RegexConsts#EMAIL
*/
public static final Pattern EMAIL = Pattern.compile(RegexConsts.EMAIL, Pattern.CASE_INSENSITIVE);
/**
* 中国大陆手机号
*
* @see RegexConsts#MOBILE_PHONE
*/
public static final Pattern MOBILE_PHONE = Pattern.compile(RegexConsts.MOBILE_PHONE);
/**
* 用户名
*
* @see RegexConsts#USERNAME
*/
public static final Pattern USERNAME = Pattern.compile(RegexConsts.USERNAME);
/**
* 昵称
*
* @see RegexConsts#NICKNAME
*/
public static final Pattern NICKNAME = Pattern.compile(RegexConsts.NICKNAME);
/**
* 中国第二代居民身份证
*
* @see RegexConsts#CHINESE_2ND_ID_CARD_NUMBER
*/
public static final Pattern CHINESE_2ND_ID_CARD_NUMBER
= Pattern.compile(RegexConsts.CHINESE_2ND_ID_CARD_NUMBER, Pattern.CASE_INSENSITIVE);
private PatternConsts() {
throw new IllegalStateException("Utility class");
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2022-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.constant;
/**
* 正则表达式常量
*
* @author ZhouXY
* @see PatternConsts
*/
public final class RegexConsts {
public static final String BASIC_ISO_DATE = "^(?<yyyy>\\d{4,9})(?<MM>\\d{2})(?<dd>\\d{2})";
public static final String ISO_LOCAL_DATE = "^(?<yyyy>\\d{4,9})-(?<MM>\\d{2})-(?<dd>\\d{2})";
public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])[\\w\\\\!#$%&'*\\+\\-/=?^`{|}~@\\(\\)\\[\\]\",\\.;':><]{8,32}$";
public static final String CAPTCHA = "^\\w{4,6}$";
/**
* from https://emailregex.com/
*/
public static final String EMAIL
= "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")"
+ "@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])";
public static final String MOBILE_PHONE = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$";
public static final String USERNAME = "^[\\w-_.@]{4,36}$";
public static final String NICKNAME = "^[\\w-_.@]{4,36}$";
public static final String CHINESE_2ND_ID_CARD_NUMBER
= "^(?<county>(?<city>(?<province>\\d{2})\\d{2})\\d{2})(?<birthDate>\\d{8})\\d{2}(?<gender>\\d)([\\dX])$";
private RegexConsts() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* <h2>常量</h2>
*
* <h3>
* 1. 正则常量
* </h3>
* {@link RegexConsts} 包含常见正则表达式;{@link PatternConsts} 包含对应的 {@link Pattern} 对象。
*
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.constant;
import java.util.regex.Pattern;

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception;
/**
* 数据不存在异常
*
* @author ZhouXY
* @since 1.0.0
*/
public final class DataNotExistsException extends Exception {
private static final long serialVersionUID = 6536955800679703111L;
/**
* 使用默认 message 构造新的 {@code DataNotExistsException}。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*/
public DataNotExistsException() {
super();
}
/**
* 使用指定的 {@code message} 构造新的 {@code DataNotExistsException}。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*
* @param message 异常信息
*/
public DataNotExistsException(String message) {
super(message);
}
/**
* 使用指定的 {@code cause} 构造新的 {@code DataNotExistsException}。
* {@code message} 为 (cause==null ? null : cause.toString())。
*
* @param cause 包装的异常
*/
public DataNotExistsException(Throwable cause) {
super(cause);
}
/**
* 使用指定的 {@code message} 和 {@code cause} 构造新的 {@code DataNotExistsException}。
*
* @param message 异常信息
* @param cause 包装的异常
*/
public DataNotExistsException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception;
import javax.annotation.Nonnull;
/**
* 异常工厂
*
* @param <X> 异常类型
* @author ZhouXY
*/
public interface IExceptionFactory<X extends Exception> {
/**
* 创建异常
*
* @return 异常对象
*/
@Nonnull
X create();
/**
* 使用指定 {@code message} 创建异常
*
* @param message 异常信息
* @return 异常对象
*/
@Nonnull
X create(String message);
/**
* 使用指定 {@code cause} 创建异常
*
* @param cause 包装的异常
* @return 异常对象
*/
@Nonnull
X create(Throwable cause);
/**
* 使用指定 {@code message} 和 {@code cause} 创建异常
*
* @param message 异常信息
* @param cause 包装的异常
* @return 异常对象
*/
@Nonnull
X create(String message, Throwable cause);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,19 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception;
package xyz.zhouxy.plusone.commons.base;
import javax.annotation.Nonnull;
import xyz.zhouxy.plusone.commons.annotation.Virtual;
import xyz.zhouxy.plusone.commons.base.IWithCode;
/**
* 规定实现类带有 {@code getCode} 方法
* 用于像自定义异常等需要带有 {@code code} 字段的类
* 方便其它地方的程序判断该类的是否实现了此接口以此获取其实例的 {@code code} 字段的值
* 异常场景
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @param <TCode> 场景编码
* @author ZhouXY
*/
public interface IWithCode<T> {
@Nonnull
T getCode();
public interface IExceptionType<TCode> extends IWithCode<TCode> {
/**
* 默认异常信息
*
* @return 默认异常信息
*/
String getDefaultMessage();
@Virtual
default String getDescription() {
return getDefaultMessage();
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception;
import javax.annotation.Nonnull;
/**
* IMultiTypesException
*
* <p>
* 异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。
*
* <p>
* 异常实现 {@link IMultiTypesException} 的 {@link #getType} 方法,返回对应的场景类型。
*
* <p>
* 表示场景类型的枚举实现 {@link IExceptionType},各个枚举值本身就是该场景的异常的工厂实例,
* 使用其中的工厂方法用于创建对应类型的异常。
*
* <pre>
* public final class LoginException
* extends RuntimeException
* implements IMultiTypesException&lt;LoginException.Type&gt; {
* private static final long serialVersionUID = 881293090625085616L;
* private final Type type;
* private LoginException(&#64;Nonnull Type type, &#64;Nonnull String message) {
* super(message);
* this.type = type;
* }
*
* private LoginException(&#64;Nonnull Type type, &#64;Nonnull Throwable cause) {
* super(cause);
* this.type = type;
* }
*
* private LoginException(&#64;Nonnull Type type,
* &#64;Nonnull String message,
* &#64;Nonnull Throwable cause) {
* super(message, cause);
* this.type = type;
* }
*
* &#64;Override
* public &#64;Nonnull Type getType() {
* return this.type;
* }
*
* // ...
*
* public enum Type implements IExceptionType&lt;String&gt;, IExceptionFactory&lt;LoginException&gt; {
* DEFAULT("00", "当前会话未登录"),
* NOT_TOKEN("10", "未提供token"),
* INVALID_TOKEN("20", "token无效"),
* TOKEN_TIMEOUT("30", "token已过期"),
* BE_REPLACED("40", "token已被顶下线"),
* KICK_OUT("50", "token已被踢下线"),
* ;
*
* &#64;Nonnull
* private final String code;
* &#64;Nonnull
* private final String defaultMessage;
*
* Type(&#64;Nonnull String code, &#64;Nonnull String defaultMessage) {
* this.code = code;
* this.defaultMessage = defaultMessage;
* }
*
* &#64;Override
* public &#64;Nonnull String getCode() {
* return code;
* }
*
* &#64;Override
* public &#64;Nonnull String getDefaultMessage() {
* return defaultMessage;
* }
*
* &#64;Override
* public &#64;Nonnull LoginException create() {
* return new LoginException(this, this.defaultMessage);
* }
*
* &#64;Override
* public &#64;Nonnull LoginException create(String message) {
* return new LoginException(this, message);
* }
*
* &#64;Override
* public &#64;Nonnull LoginException create(Throwable cause) {
* return new LoginException(this, cause);
* }
*
* &#64;Override
* public &#64;Nonnull LoginException create(String message, Throwable cause) {
* return new LoginException(this, message, cause);
* }
* }
* }
* </pre>
*
* 使用时,可以使用这种方式创建并抛出异常:
* <pre>
* throw LoginException.Type.TOKEN_TIMEOUT.create();
* </pre>
*
* @param <T> 异常场景
* @author ZhouXY
* @since 1.0.0
*/
public interface IMultiTypesException<T extends IExceptionType<?>> {
/**
* 异常类型
*
* @return 异常类型。通常是实现了 {@link IExceptionType} 的枚举。
*/
@Nonnull
T getType();
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception;
import java.time.format.DateTimeParseException;
import javax.annotation.Nonnull;
import xyz.zhouxy.plusone.commons.exception.business.RequestParamsException;
/**
* 解析失败异常
*
* <p>
* 解析失败的不一定是客户传的参数,也可能是其它来源的数据解析失败。
* 如果表示用户传参造成的解析失败,可使用 {@link RequestParamsException#RequestParamsException(Throwable)}
* 将 ParsingFailureException 包装成 {@link RequestParamsException} 再抛出。
* <pre>
* throw new RequestParamsException(ParsingFailureException.Type.NUMBER_PARSING_FAILURE.create());
* </pre>
*
* @author ZhouXY
* @since 1.0.0
*/
public final class ParsingFailureException
extends Exception
implements IMultiTypesException<ParsingFailureException.Type> {
private static final long serialVersionUID = 795996090625132616L;
private final Type type;
private ParsingFailureException(Type type, String message) {
super(message);
this.type = type;
}
private ParsingFailureException(Type type, Throwable cause) {
super(cause);
this.type = type;
}
private ParsingFailureException(Type type, String message, Throwable cause) {
super(message, cause);
this.type = type;
}
/**
* 创建默认类型的 {@code ParsingFailureException}。
* {@code type} 为 {@link Type#DEFAULT}
* {@code message} 为 {@link Type#DEFAULT} 的默认信息。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*/
public ParsingFailureException() {
this(Type.DEFAULT, Type.DEFAULT.getDefaultMessage());
}
/**
* 使用指定 {@code message} 创建默认类型的 {@code ParsingFailureException}。
* {@code type} 为 {@link Type#DEFAULT}
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*
* @param message 异常信息
*/
public ParsingFailureException(String message) {
this(Type.DEFAULT, message);
}
/**
* 使用指定的 {@code cause} 创建默认类型的 {@code ParsingFailureException}。
* {@code type} 为 {@link Type#DEFAULT}
* {@code message} 为 (cause==null ? null : cause.toString())。
*
* @param cause 包装的异常
*/
public ParsingFailureException(Throwable cause) {
this(Type.DEFAULT, cause);
}
/**
* 使用指定的 {@code message} 和 {@code cause} 创建默认类型的 {@code ParsingFailureException}。
* {@code type} 为 {@link Type#DEFAULT}。
*
* @param message 异常信息
* @param cause 包装的异常
*/
public ParsingFailureException(String message, Throwable cause) {
this(Type.DEFAULT, message, cause);
}
/**
* 将 {@link DateTimeParseException} 包装为 {@link ParsingFailureException}。
* {@code type} 为 {@link Type#DATE_TIME_PARSING_FAILURE}。
*
* @param cause 包装的 {@code DateTimeParseException}
* @return ParsingFailureException
*/
public static ParsingFailureException of(DateTimeParseException cause) {
if (cause == null) {
return Type.DATE_TIME_PARSING_FAILURE.create();
}
return Type.DATE_TIME_PARSING_FAILURE.create(cause.getMessage(), cause);
}
/**
* 将 {@link DateTimeParseException} 包装为 {@link ParsingFailureException}。
* {@code type} 为 {@link Type#DATE_TIME_PARSING_FAILURE}。
*
* @param message 异常信息
* @param cause 包装的 {@code DateTimeParseException}
* @return ParsingFailureException
*/
public static ParsingFailureException of(String message, DateTimeParseException cause) {
return Type.DATE_TIME_PARSING_FAILURE.create(message, cause);
}
/**
* 将 {@link NumberFormatException} 包装为 {@link ParsingFailureException}。
* {@code type} 为 {@link Type#NUMBER_PARSING_FAILURE}。
*
* @param cause 包装的 {@code NumberFormatException}
* @return ParsingFailureException
*/
public static ParsingFailureException of(NumberFormatException cause) {
if (cause == null) {
return Type.NUMBER_PARSING_FAILURE.create();
}
return Type.NUMBER_PARSING_FAILURE.create(cause.getMessage(), cause);
}
/**
* 将 {@link NumberFormatException} 包装为 {@link ParsingFailureException}。
* {@code type} 为 {@link Type#NUMBER_PARSING_FAILURE}。
*
* @param message 异常信息
* @param cause {@code NumberFormatException}
* @return ParsingFailureException
*/
public static ParsingFailureException of(String message, NumberFormatException cause) {
return Type.NUMBER_PARSING_FAILURE.create(message, cause);
}
@Override
@Nonnull
public Type getType() {
return type;
}
/** 默认类型 */
public static final Type DEFAULT = Type.DEFAULT;
/** 数字转换失败 */
public static final Type NUMBER_PARSING_FAILURE = Type.NUMBER_PARSING_FAILURE;
/** 时间解析失败 */
public static final Type DATE_TIME_PARSING_FAILURE = Type.DATE_TIME_PARSING_FAILURE;
/** JSON 解析失败 */
public static final Type JSON_PARSING_FAILURE = Type.JSON_PARSING_FAILURE;
/** XML 解析失败 */
public static final Type XML_PARSING_FAILURE = Type.XML_PARSING_FAILURE;
public enum Type implements IExceptionType<String>, IExceptionFactory<ParsingFailureException> {
DEFAULT("00", "解析失败"),
NUMBER_PARSING_FAILURE("10", "数字转换失败"),
DATE_TIME_PARSING_FAILURE("20", "时间解析失败"),
JSON_PARSING_FAILURE("30", "JSON 解析失败"),
XML_PARSING_FAILURE("40", "XML 解析失败"),
;
@Nonnull
private final String code;
@Nonnull
private final String defaultMessage;
Type(@Nonnull String code, @Nonnull String defaultMessage) {
this.code = code;
this.defaultMessage = defaultMessage;
}
@Override
public @Nonnull String getCode() {
return code;
}
@Override
public @Nonnull String getDefaultMessage() {
return defaultMessage;
}
@Override
public @Nonnull ParsingFailureException create() {
return new ParsingFailureException(this, this.defaultMessage);
}
@Override
public @Nonnull ParsingFailureException create(String message) {
return new ParsingFailureException(this, message);
}
@Override
public @Nonnull ParsingFailureException create(Throwable cause) {
return new ParsingFailureException(this, cause);
}
@Override
public @Nonnull ParsingFailureException create(String message, Throwable cause) {
return new ParsingFailureException(this, message, cause);
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception.business;
/**
* BizException
*
* <p>
* 业务异常
*
* <p>
* <b>NOTE: 通常表示业务中的意外情况。如:用户错误输入、缺失必填字段、用户余额不足等。</b>
*
* @author ZhouXY
* @since 1.0.0
*/
public class BizException extends RuntimeException {
private static final long serialVersionUID = 982585090625482416L;
private static final String DEFAULT_MSG = "业务异常";
/**
* 使用指定的 {@code message} 构造新的业务异常。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*
* @param message 异常信息
*/
protected BizException(String message) {
super(message);
}
/**
* 使用指定的 {@code cause} 构造新的业务异常。
* {@code message} 为 (cause==null ? null : cause.toString())。
*
* @param cause 包装的异常
*/
protected BizException(Throwable cause) {
super(cause);
}
/**
* 使用指定的 {@code message} 和 {@code cause} 构造新的业务异常。
*
* @param message 异常信息
* @param cause 包装的异常
*/
protected BizException(String message, Throwable cause) {
super(message, cause);
}
public static BizException of() {
return new BizException(DEFAULT_MSG);
}
public static BizException of(String message) {
return new BizException(message);
}
public static BizException of(String errorMessageFormat, Object... errorMessageArgs) {
return new BizException(String.format(errorMessageFormat, errorMessageArgs));
}
public static BizException of(Throwable cause) {
return new BizException(cause);
}
public static BizException of(String message, Throwable cause) {
return new BizException(message, cause);
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception.business;
import javax.annotation.Nonnull;
import xyz.zhouxy.plusone.commons.exception.IExceptionFactory;
import xyz.zhouxy.plusone.commons.exception.IExceptionType;
import xyz.zhouxy.plusone.commons.exception.IMultiTypesException;
/**
* InvalidInputException
*
* <p>
* 用户输入内容非法
*
* <p>
* <b>NOTE: 属业务异常</b>
*
* @author ZhouXY
* @since 1.0.0
*/
public final class InvalidInputException
extends RequestParamsException
implements IMultiTypesException<InvalidInputException.Type> {
private static final long serialVersionUID = -28994090625082516L;
private final Type type;
private InvalidInputException(Type type) {
super(type.getDefaultMessage());
this.type = type;
}
private InvalidInputException(Type type, String message) {
super(message);
this.type = type;
}
private InvalidInputException(Type type, Throwable cause) {
super(cause);
this.type = type;
}
private InvalidInputException(Type type, String message, Throwable cause) {
super(message, cause);
this.type = type;
}
/**
* 创建默认类型的 {@code InvalidInputException}。
* {@code type} 为 {@link Type#DEFAULT}
* {@code message} 为 {@link Type#DEFAULT} 的默认信息。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*/
public InvalidInputException() {
this(Type.DEFAULT);
}
/**
* 使用指定 {@code message} 创建默认类型的 {@code InvalidInputException}。
* {@code type} 为 {@link Type#DEFAULT}
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*
* @param message 异常信息
*/
public InvalidInputException(String message) {
this(Type.DEFAULT, message);
}
/**
* 使用指定的 {@code cause} 创建默认类型的 {@code InvalidInputException}。
* {@code type} 为 {@link Type#DEFAULT}
* {@code message} 为 (cause==null ? null : cause.toString())。
*
* @param cause 包装的异常
*/
public InvalidInputException(Throwable cause) {
this(Type.DEFAULT, cause);
}
/**
* 使用指定的 {@code message} 和 {@code cause} 创建默认类型的 {@code InvalidInputException}。
* {@code type} 为 {@link Type#DEFAULT}。
*
* @param message 异常信息
* @param cause 包装的异常
*/
public InvalidInputException(String message, Throwable cause) {
this(Type.DEFAULT, message, cause);
}
@Override
@Nonnull
public Type getType() {
return this.type;
}
public enum Type implements IExceptionType<String>, IExceptionFactory<InvalidInputException> {
DEFAULT("00", "用户输入内容非法"),
CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS("01", "包含非法恶意跳转链接"),
CONTAINS_ILLEGAL_WORDS("02", "包含违禁敏感词"),
PICTURE_CONTAINS_ILLEGAL_INFORMATION("03", "图片包含违禁信息"),
INFRINGE_COPYRIGHT("04", "文件侵犯版权"),
;
@Nonnull
private final String code;
@Nonnull
private final String defaultMessage;
Type(@Nonnull String code, @Nonnull String defaultMsg) {
this.code = code;
this.defaultMessage = defaultMsg;
}
@Override
public @Nonnull String getCode() {
return code;
}
@Override
public @Nonnull String getDefaultMessage() {
return defaultMessage;
}
@Override
public @Nonnull InvalidInputException create() {
return new InvalidInputException(this);
}
@Override
public @Nonnull InvalidInputException create(String message) {
return new InvalidInputException(this, message);
}
@Override
public @Nonnull InvalidInputException create(Throwable cause) {
return new InvalidInputException(this, cause);
}
@Override
public @Nonnull InvalidInputException create(String message, Throwable cause) {
return new InvalidInputException(this, message, cause);
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception.business;
/**
* RequestParamsException
*
* <p>
* 用户请求参数错误
*
* @author ZhouXY
* @since 1.0.0
*/
public class RequestParamsException extends BizException {
private static final long serialVersionUID = 448337090625192516L;
private static final String DEFAULT_MSG = "用户请求参数错误";
/**
* 使用默认 message 构造新的 {@code RequestParamsException}。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*/
public RequestParamsException() {
super(DEFAULT_MSG);
}
/**
* 使用指定的 {@code message} 构造新的 {@code RequestParamsException}。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*
* @param message 异常信息
*/
public RequestParamsException(String message) {
super(message);
}
/**
* 使用指定的 {@code cause} 构造新的 {@code RequestParamsException}。
* {@code message} 为 (cause==null ? null : cause.toString())。
*
* @param cause 包装的异常
*/
public RequestParamsException(Throwable cause) {
super(cause);
}
/**
* 使用指定的 {@code message} 和 {@code cause} 构造新的 {@code RequestParamsException}。
*
* @param message 异常信息
* @param cause 包装的异常
*/
public RequestParamsException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 业务异常
*
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.exception.business;

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 包含常见的业务异常与系统异常,以及异常相关的工具
*
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.exception;

View File

@@ -0,0 +1,288 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception.system;
import java.util.function.Supplier;
import javax.annotation.Nullable;
/**
* JdbcUpdateAffectedIncorrectNumberOfRowsException
*
* <p>
* 当数据操作的结果不符合预期时抛出。
*
* <p>
* 比如当一个 insert 或 update 操作时,预计影响数据库中的一行数据,但结果却影响了零条数据或多条数据,
* 当出现这种始料未及的诡异情况时,抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException} 并回滚事务。
* 后续需要排查原因。
*
* @author ZhouXY
* @since 1.0.0
*/
public final class JdbcUpdateAffectedIncorrectNumberOfRowsException extends SysException {
private static final long serialVersionUID = 992754090625352516L;
private final long expected;
private final long actual;
/**
* 创建一个 {@code JdbcUpdateAffectedIncorrectNumberOfRowsException} 对象
*
* @param expected 预期影响的行数
* @param actual 实际影响的行数
*/
public JdbcUpdateAffectedIncorrectNumberOfRowsException(long expected, long actual) {
super(String.format("The number of rows affected is expected to be %d, but is: %d", expected, actual));
this.expected = expected;
this.actual = actual;
}
/**
* 创建一个 {@code JdbcUpdateAffectedIncorrectNumberOfRowsException} 对象
*
* @param expected 预期影响的行数
* @param actual 实际影响的行数
* @param message 错误信息
*/
public JdbcUpdateAffectedIncorrectNumberOfRowsException(long expected, long actual, String message) {
super(message);
this.expected = expected;
this.actual = actual;
}
/**
* 预期影响的行数
*
* @return the expected
*/
public long getExpected() {
return expected;
}
/**
* 实际影响的行数
*
* @return the actual
*/
public long getActual() {
return actual;
}
// ================================
// #region - AffectedRows
// ================================
/**
* 当影响的数据量与预计不同时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
*/
public static void checkAffectedRows(int expected, int actualRowCount) {
if (expected != actualRowCount) {
throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(expected, actualRowCount);
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessage 异常信息
*/
public static void checkAffectedRows(int expected, int actualRowCount,
@Nullable String errorMessage) {
if (expected != actualRowCount) {
throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(expected, actualRowCount, errorMessage);
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessageSupplier 异常信息
*/
public static void checkAffectedRows(int expected, int actualRowCount,
Supplier<String> errorMessageSupplier) {
if (expected != actualRowCount) {
throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(expected, actualRowCount, errorMessageSupplier.get());
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
*/
public static void checkAffectedRows(int expected, int actualRowCount,
String errorMessageTemplate, Object... errorMessageArgs) {
if (expected != actualRowCount) {
throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(expected, actualRowCount,
String.format(errorMessageTemplate, errorMessageArgs));
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
*/
public static void checkAffectedRows(long expected, long actualRowCount) {
if (expected != actualRowCount) {
throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(expected, actualRowCount);
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessage 异常信息
*/
public static void checkAffectedRows(long expected, long actualRowCount,
@Nullable String errorMessage) {
if (expected != actualRowCount) {
throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(expected, actualRowCount, errorMessage);
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessageSupplier 异常信息
*/
public static void checkAffectedRows(long expected, long actualRowCount,
Supplier<String> errorMessageSupplier) {
if (expected != actualRowCount) {
throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(expected, actualRowCount, errorMessageSupplier.get());
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
*/
public static void checkAffectedRows(long expected, long actualRowCount,
String errorMessageTemplate, Object... errorMessageArgs) {
if (expected != actualRowCount) {
throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(expected, actualRowCount,
String.format(errorMessageTemplate, errorMessageArgs));
}
}
/**
* 当影响的数据量不为 1 时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param actualRowCount 实际影响的行数
*/
public static void checkAffectedOneRow(int actualRowCount) {
checkAffectedRows(1, actualRowCount);
}
/**
* 当影响的数据量不为 1 时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessage 异常信息
*/
public static void checkAffectedOneRow(int actualRowCount, String errorMessage) {
checkAffectedRows(1, actualRowCount, errorMessage);
}
/**
* 当影响的数据量不为 1 时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessageSupplier 异常信息
*/
public static void checkAffectedOneRow(int actualRowCount, Supplier<String> errorMessageSupplier) {
checkAffectedRows(1, actualRowCount, errorMessageSupplier);
}
/**
* 当影响的数据量不为 1 时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
*/
public static void checkAffectedOneRow(int actualRowCount,
String errorMessageTemplate, Object... errorMessageArgs) {
checkAffectedRows(1, actualRowCount, errorMessageTemplate, errorMessageArgs);
}
/**
* 当影响的数据量不为 1 时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param result 实际影响的数据量
*/
public static void checkAffectedOneRow(long result) {
checkAffectedRows(1L, result);
}
/**
* 当影响的数据量不为 1 时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessage 异常信息
*/
public static void checkAffectedOneRow(long actualRowCount, String errorMessage) {
checkAffectedRows(1L, actualRowCount, errorMessage);
}
/**
* 当影响的数据量不为 1 时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessageSupplier 异常信息
*/
public static void checkAffectedOneRow(long actualRowCount, Supplier<String> errorMessageSupplier) {
checkAffectedRows(1L, actualRowCount, errorMessageSupplier);
}
/**
* 当影响的数据量不为 1 时抛出 {@link JdbcUpdateAffectedIncorrectNumberOfRowsException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
*/
public static void checkAffectedOneRow(long actualRowCount,
String errorMessageTemplate, Object... errorMessageArgs) {
checkAffectedRows(1L, actualRowCount, errorMessageTemplate, errorMessageArgs);
}
// ================================
// #endregion - AffectedRows
// ================================
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception.system;
/**
* NoAvailableMacFoundException
*
* <p>
* 在无法找到可访问的 Mac 地址时抛出
*
* @author ZhouXY
* @since 1.0.0
*/
public class NoAvailableMacFoundException extends SysException {
private static final long serialVersionUID = 152827098461071551L;
/**
* 使用默认 message 构造新的 {@code NoAvailableMacFoundException}。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*/
public NoAvailableMacFoundException() {
super("无法找到可访问的 Mac 地址");
}
/**
* 使用指定的 {@code message} 构造新的 {@code NoAvailableMacFoundException}。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*
* @param message 异常信息
*/
public NoAvailableMacFoundException(String message) {
super(message);
}
/**
* 使用指定的 {@code cause} 构造新的 {@code NoAvailableMacFoundException}。
* {@code message} 为 (cause==null ? null : cause.toString())。
*
* @param cause 包装的异常
*/
public NoAvailableMacFoundException(Throwable cause) {
super(cause);
}
/**
* 使用指定的 {@code message} 和 {@code cause} 构造新的 {@code NoAvailableMacFoundException}。
*
* @param message 异常信息
* @param cause 包装的异常
*/
public NoAvailableMacFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.exception.system;
/**
* 系统异常
*
* <p>
* 通常表示应用代码存在问题,或因环境问题,引发异常。
*
* @author ZhouXY
* @since 1.0.0
*/
public class SysException extends RuntimeException {
private static final long serialVersionUID = -936435090625482516L;
private static final String DEFAULT_MSG = "系统异常";
/**
* 使用指定的 {@code message} 构造新的系统异常。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*
* @param message 异常信息
*/
protected SysException(String message) {
super(message);
}
/**
* 使用指定的 {@code cause} 构造新的系统异常。
* {@code message} 为 (cause==null ? null : cause.toString())。
*
* @param cause 包装的异常
*/
protected SysException(Throwable cause) {
super(cause);
}
/**
* 使用指定的 {@code message} 和 {@code cause} 构造新的系统异常。
*
* @param message 异常信息
* @param cause 包装的异常
*/
protected SysException(String message, Throwable cause) {
super(message, cause);
}
public static SysException of() {
return new SysException(DEFAULT_MSG);
}
public static SysException of(String message) {
return new SysException(message);
}
public static SysException of(String errorMessageFormat, Object... errorMessageArgs) {
return new SysException(String.format(errorMessageFormat, errorMessageArgs));
}
public static SysException of(Throwable cause) {
return new SysException(cause);
}
public static SysException of(String message, Throwable cause) {
return new SysException(message, cause);
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 系统异常
*
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.exception.system;

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.function;
import com.google.common.annotations.Beta;
/**
* BoolUnaryOperator
*
* <p>
* 一个特殊的 {@link java.util.function.UnaryOperator}。
* 表示对 {@code boolean} 值的一元操作。
*
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.UnaryOperator
*/
@Beta
@FunctionalInterface
public interface BoolUnaryOperator {
/**
* 将此函数应用于给定的 {@code boolean} 参数,返回一个 {@code boolean} 结果。
*
* @param operand 操作数
* @return 结果
*/
boolean applyAsBool(boolean operand);
/**
* 返回一个 {@code BoolUnaryOperator},该操作符将给定的操作数取反。
*
* @return {@code BoolUnaryOperator}
*/
static BoolUnaryOperator not() {
return b -> !b;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,28 @@
package xyz.zhouxy.plusone.commons.function;
import java.util.OptionalLong;
import java.util.function.Function;
import com.google.common.annotations.Beta;
/**
* ToOptionalLongFunction
* CharUnaryOperator
*
* <p>
* 接受类型为 T 的参数返回 {@link OptionalLong} 对象
* 一个特殊的 {@link java.util.function.UnaryOperator}
* 表示对 {@code char} 的一元操作
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @see OptionalLong
* @see Function
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.UnaryOperator
*/
@Beta
@FunctionalInterface
public interface ToOptionalLongFunction<T> extends Function<T, OptionalLong> {
public interface CharUnaryOperator {
/**
* 将此函数应用于给定的 {@code char} 参数返回一个 {@code char} 结果
*
* @param operand 操作数
* @return 结果
*/
char applyAsChar(char operand);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,25 @@
package xyz.zhouxy.plusone.commons.function;
import java.util.OptionalInt;
import java.util.function.Supplier;
/**
* OptionalIntSupplier
* Executable
*
* <p>
* 返回 {@link OptionalInt} 对象
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @see OptionalInt
* @see Supplier
* 表示一个无入参无返回值的操作可抛出异常
*
* @param <E> 可抛出的异常类型
*
* @author ZhouXY
* @since 1.0.0
*/
@FunctionalInterface
public interface OptionalIntSupplier extends Supplier<OptionalInt> {
public interface Executable<E extends Throwable> {
/**
* 执行
*
* @throws E 可抛出的异常
*/
void execute() throws E;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,9 +24,9 @@ import java.util.function.Supplier;
*
* <p>
* 返回 {@code Optional&lt;T&gt;} 对象
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
*
* @author ZhouXY
* @since 1.0.0
* @see Optional
* @see Supplier
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,38 +19,37 @@ package xyz.zhouxy.plusone.commons.function;
import java.util.function.Predicate;
/**
* Predicates
* PredicateTools
*
* <p>
* {@link Predicate} 相关操作
* </p>
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @author ZhouXY
* @since 1.0.0
* @see Predicate
*/
public class Predicates {
public class PredicateTools {
/**
* lambda 表达式或者方法引用指明为对应类型的 {@link Predicate} 对象
* 如将 {@code Objects::nonNull} 明确地指定为 {@code Predicate&lt;String&gt;}
* 使之可以链式调用 {@link Predicate#and(Predicate)}{@link Predicate#or(Predicate)}
* 等方法连接其它 {@code Predicate<? super T>} 对象
*
*
* <pre>
* Predicate&lt;String&gt; predicate = Predicates.&lt;String&gt;of(Objects::nonNull)
* Predicate&lt;String&gt; predicate = PredicateTools.&lt;String&gt;from(Objects::nonNull)
* .and(StringUtils::isNotEmpty);
* </pre>
*
*
* @param <T> 目标类型
* @param predicate {@link Predicate} 实例
* @return 包装的 {@link Predicate} 实例
* @param predicate Lambda 表达式
* @return 传入的表达式自动成为 {@link Predicate} 实例
*/
public static <T> Predicate<T> of(Predicate<? super T> predicate) {
return predicate::test;
public static <T> Predicate<T> from(Predicate<T> predicate) {
return predicate;
}
private Predicates() {
private PredicateTools() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,25 @@
package xyz.zhouxy.plusone.commons.function;
import java.util.Optional;
import java.util.function.DoubleFunction;
/**
* DoubleToOptionalFunction
* ThrowingConsumer
*
* <p>
* 接受类型为 double 的参数返回 {@code Optional&lt;R&gt;} 对象
* 允许抛出异常的消费操作是一个特殊的 {@link java.util.function.Consumer}
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @see Optional
* @see DoubleFunction
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.Consumer
*/
@FunctionalInterface
public interface DoubleToOptionalFunction<R> extends DoubleFunction<Optional<R>> {
public interface ThrowingConsumer<T, E extends Throwable> {
/**
* 消费给定的参数允许抛出异常
*
* @param t 要消费的参数
* @throws E 抛出的异常
*/
void accept(T t) throws E;
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.function;
/**
* ThrowingFunction
*
* <p>
* 接收一个参数,并返回一个结果,可以抛出异常。
*
* @param <T> 入参类型
* @param <R> 返回结果类型
* @param <E> 异常类型
*
* @author ZhouXY
* @since 1.0
* @see java.util.function.Function
*/
@FunctionalInterface
public interface ThrowingFunction<T, R, E extends Throwable> {
/**
* 接收一个参数,并返回一个结果,可以抛出异常。
*
* @param t 入参
* @return 函数结果
* @throws E 抛出的异常
*/
R apply(T t) throws E;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,25 @@
package xyz.zhouxy.plusone.commons.function;
import java.util.OptionalInt;
import java.util.function.Function;
/**
* ToOptionalIntFunction
* ThrowingPredicate
*
* <p>
* 受类型为 T 的参数返回 {@link OptionalInt} 对象
* 收一个参数返回一个布尔值可抛出异常
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @see OptionalInt
* @see Function
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.Predicate
*/
@FunctionalInterface
public interface ToOptionalIntFunction<T> extends Function<T, OptionalInt> {
public interface ThrowingPredicate<T, E extends Throwable> {
/**
* 对给定的参数进行评估
*
* @param t 入参
* @return 入参符合条件时返回 {@code true}否则返回 {@code false}
* @throws E 抛出的异常
*/
boolean test(T t) throws E;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,28 @@
package xyz.zhouxy.plusone.commons.function;
import java.util.OptionalDouble;
import java.util.function.Function;
/**
* ToOptionalDoubleFunction
* ThrowingSupplier
*
* <p>
* 接受类型为 T 的参数返回 {@link OptionalDouble} 对象
* 允许抛出异常的 Supplier 接口
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @see OptionalDouble
* @see Function
* @param <T> 结果类型
* @param <E> 异常类型
*
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.Supplier
*/
@FunctionalInterface
public interface ToOptionalDoubleFunction<T> extends Function<T, OptionalDouble> {
public interface ThrowingSupplier<T, E extends Throwable> {
/**
* 获取一个结果允许抛出异常
*
* @return 结果
* @throws E 允许抛出的异常
*/
T get() throws E;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,8 +25,8 @@ import java.util.function.BiFunction;
* <p>
* 接受类型为 T U 的两个参数返回 {@code Optional&lt;R&gt;} 对象
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @author ZhouXY
* @since 1.0.0
* @see Optional
* @see BiFunction
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,8 +25,8 @@ import java.util.function.Function;
* <p>
* 接受类型为 T 的参数返回 {@code Optional&lt;R&gt;} 对象
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @author ZhouXY
* @since 1.0.0
* @see Optional
* @see Function
*/

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* <h2>函数式编程</h2>
*
* <h3>1. PredicateTools</h3>
* <p>
* {@link PredicateTools} 用于 {@link java.util.function.Predicate} 的相关操作。
*
* <h3>2. Functional interfaces</h3>
* <p>
* 补充可能用得上的函数式接口:
* <pre>
* | Group | FunctionalInterface | method |
* | ------------- | -------------------- | -------------------------------- |
* | UnaryOperator | BoolUnaryOperator | boolean applyAsBool (boolean) |
* | UnaryOperator | CharUnaryOperator | char applyAsChar(char) |
* | Throwing | Executable | void execute() throws E |
* | Throwing | ThrowingConsumer | void accept(T) throws E |
* | Throwing | ThrowingFunction | R apply(T) throws E |
* | Throwing | ThrowingPredicate | boolean test(T) throws E |
* | Throwing | ThrowingSupplier | T get() throws E |
* | Optional | OptionalSupplier | Optional&lt;T&gt; get() throws E |
* | Optional | ToOptionalBiFunction | Optional&lt;R&gt; apply(T,U) |
* | Optional | ToOptionalFunction | Optional&lt;R&gt; apply(T) |
* </pre>
*
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.function;

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.gson.adapter;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQuery;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* 包含 JSR-310 相关数据类型的 {@code TypeAdapter}
*
* @author ZhouXY
* @since 1.1.0
* @see TypeAdapter
* @see com.google.gson.GsonBuilder
*/
public class JSR310TypeAdapters {
/**
* {@code LocalDate} 的 {@code TypeAdapter}
* 用于 Gson 对 {@code LocalDate} 进行相互转换。
*/
public static final class LocalDateTypeAdapter
extends TemporalAccessorTypeAdapter<LocalDate, LocalDateTypeAdapter> {
/**
* 默认构造函数,
* 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE} 进行 {@link LocalDate} 的序列化与反序列化。
*/
public LocalDateTypeAdapter() {
this(DateTimeFormatter.ISO_LOCAL_DATE);
}
/**
* 构造函数,
* 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDate} 的序列化与反序列化。
*
* @param formatter 用于序列化 {@link LocalDate} 的格式化器,不可为 {@code null}。
*/
public LocalDateTypeAdapter(DateTimeFormatter formatter) {
super(LocalDate::from, formatter);
}
}
/**
* {@code LocalDateTime} 的 {@code TypeAdapter}
* 用于 Gson 对 {@code LocalDateTime} 进行相互转换。
*/
public static final class LocalDateTimeTypeAdapter
extends TemporalAccessorTypeAdapter<LocalDateTime, LocalDateTimeTypeAdapter> {
/**
* 默认构造函数,
* 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} 进行 {@link LocalDateTime} 的序列化与反序列化。
*/
public LocalDateTimeTypeAdapter() {
this(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
/**
* 构造函数,
* 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDateTime} 的序列化与反序列化。
*
* @param formatter 用于序列化 {@link LocalDateTime} 的格式化器,不可为 {@code null}。
*/
public LocalDateTimeTypeAdapter(DateTimeFormatter formatter) {
super(LocalDateTime::from, formatter);
}
}
/**
* {@code ZonedDateTime} 的 {@code TypeAdapter}
* 用于 Gson 对 {@code ZonedDateTime} 进行相互转换。
*/
public static final class ZonedDateTimeTypeAdapter
extends TemporalAccessorTypeAdapter<ZonedDateTime, ZonedDateTimeTypeAdapter> {
/**
* 默认构造函数,
* 使用 {@link DateTimeFormatter#ISO_ZONED_DATE_TIME} 进行 {@link ZonedDateTime} 的序列化与反序列化。
*/
public ZonedDateTimeTypeAdapter() {
this(DateTimeFormatter.ISO_ZONED_DATE_TIME);
}
/**
* 构造函数,
* 使用传入的 {@link DateTimeFormatter} 进行 {@link ZonedDateTime} 的序列化与反序列化。
*
* @param formatter 用于序列化 {@link ZonedDateTime} 的格式化器,不可为 {@code null}。
*/
public ZonedDateTimeTypeAdapter(DateTimeFormatter formatter) {
super(ZonedDateTime::from, formatter);
}
}
/**
* {@code Instant} 的 {@code TypeAdapter}
* 用于 Gson 对 {@code Instant} 进行相互转换。
*
* <p>
* 使用 {@link DateTimeFormatter#ISO_INSTANT} 进行 {@link Instant} 的序列化与反序列化。
*
*/
public static final class InstantTypeAdapter
extends TemporalAccessorTypeAdapter<Instant, InstantTypeAdapter> {
public InstantTypeAdapter() {
super(Instant::from, DateTimeFormatter.ISO_INSTANT);
}
}
private abstract static class TemporalAccessorTypeAdapter<
T extends TemporalAccessor,
TTypeAdapter extends TemporalAccessorTypeAdapter<T, TTypeAdapter>>
extends TypeAdapter<T> {
private final TemporalQuery<T> temporalQuery;
private final DateTimeFormatter dateTimeFormatter;
protected TemporalAccessorTypeAdapter(
TemporalQuery<T> temporalQuery, DateTimeFormatter dateTimeFormatter) {
checkArgumentNotNull(dateTimeFormatter, "formatter must not be null.");
this.temporalQuery = temporalQuery;
this.dateTimeFormatter = dateTimeFormatter;
}
/** {@inheritDoc} */
@Override
public void write(JsonWriter out, T value) throws IOException {
out.value(dateTimeFormatter.format(value));
}
/** {@inheritDoc} */
@Override
public T read(JsonReader in) throws IOException {
return dateTimeFormatter.parse(in.nextString(), temporalQuery);
}
}
private JSR310TypeAdapters() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Gson 相关类型适配器
*/
package xyz.zhouxy.plusone.commons.gson.adapter;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Gson 相关辅助工具
*/
package xyz.zhouxy.plusone.commons.gson;

View File

@@ -0,0 +1,283 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import javax.annotation.Nullable;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.Immutable;
import xyz.zhouxy.plusone.commons.annotation.ReaderMethod;
import xyz.zhouxy.plusone.commons.annotation.ValueObject;
import xyz.zhouxy.plusone.commons.constant.PatternConsts;
import xyz.zhouxy.plusone.commons.util.StringTools;
/**
* Chinese2ndGenIDCardNumber
*
* <p>
* 中国第二代居民身份证号
*
* @author ZhouXY
* @since 1.0.0
* @see xyz.zhouxy.plusone.commons.constant.PatternConsts#CHINESE_2ND_ID_CARD_NUMBER
*/
@ValueObject
@Immutable
public class Chinese2ndGenIDCardNumber
implements IDCardNumber, Comparable<Chinese2ndGenIDCardNumber>, Serializable {
private static final long serialVersionUID = 5655592250204184210L;
/** 身份证号码 */
private final String value;
/** 省份编码 */
private final String provinceCode;
/** 市级编码 */
private final String cityCode;
/** 县级编码 */
private final String countyCode;
/** 性别 */
private final Gender gender;
/** 出生日期 */
private final LocalDate birthDate;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private Chinese2ndGenIDCardNumber(String value,
String provinceCode, String cityCode, String countyCode,
Gender gender, LocalDate birthDate) {
this.value = value;
this.provinceCode = provinceCode;
this.cityCode = cityCode;
this.countyCode = countyCode;
this.gender = gender;
this.birthDate = birthDate;
}
/**
* 根据身份证号码创建 {@code Chinese2ndGenIDCardNumber} 对象
*
* @param idCardNumber 身份证号码值
* @return {@code Chinese2ndGenIDCardNumber} 对象
*/
public static Chinese2ndGenIDCardNumber of(final String idCardNumber) {
try {
checkArgument(StringTools.isNotBlank(idCardNumber), "二代居民身份证校验失败:号码为空");
final String value = idCardNumber.toUpperCase();
final Matcher matcher = PatternConsts.CHINESE_2ND_ID_CARD_NUMBER.matcher(value);
checkArgument(matcher.matches(), () -> "二代居民身份证校验失败:" + value);
final String provinceCode = matcher.group("province");
checkArgument(Chinese2ndGenIDCardNumber.PROVINCE_CODES.containsKey(provinceCode));
final String cityCode = matcher.group("city");
final String countyCode = matcher.group("county");
// 出生日期
final String birthDateStr = matcher.group("birthDate");
final LocalDate birthDate = LocalDate.parse(birthDateStr, DATE_FORMATTER);
// 性别
final int genderCode = Integer.parseInt(matcher.group("gender"));
final Gender gender = genderCode % 2 == 0 ? Gender.FEMALE : Gender.MALE;
return new Chinese2ndGenIDCardNumber(value, provinceCode, cityCode, countyCode, gender, birthDate);
}
catch (IllegalArgumentException e) {
throw e;
}
catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
// ================================
// #region - reader methods
// ================================
@Override
@ReaderMethod
public String value() {
return value;
}
/**
* 所属省份代码
*
* @return 所属省份代码
*/
@ReaderMethod
public String getProvinceCode() {
return provinceCode;
}
/**
* 所属省份名称
*
* @return 所属省份名称
*/
@ReaderMethod
public String getProvinceName() {
return PROVINCE_CODES.get(this.provinceCode);
}
/**
* 所属省份完整行政区划代码
*
* @return 所属省份完整行政区划代码
*/
@ReaderMethod
public String getFullProvinceCode() {
return Strings.padEnd(this.provinceCode, 12, '0');
}
/**
* 所属市级代码
*
* @return 所属市级代码
*/
@ReaderMethod
public String getCityCode() {
return cityCode;
}
/**
* 所属市级完整行政区划代码
*
* @return 所属市级完整行政区划代码
*/
@ReaderMethod
public String getFullCityCode() {
return Strings.padEnd(this.cityCode, 12, '0');
}
/**
* 所属县级代码
*
* @return 所属县级代码
*/
@ReaderMethod
public String getCountyCode() {
return countyCode;
}
/**
* 所属县级完整行政区划代码
*
* @return 所属县级完整行政区划代码
*/
@ReaderMethod
public String getFullCountyCode() {
return Strings.padEnd(this.countyCode, 12, '0');
}
@ReaderMethod
@Override
public Gender getGender() {
return gender;
}
@ReaderMethod
@Override
public LocalDate getBirthDate() {
return birthDate;
}
// ================================
// #endregion - reader methods
// ================================
/** 省份代码表 */
public static final Map<String, String> PROVINCE_CODES = ImmutableMap.<String, String>builder()
.put("11", "北京")
.put("12", "天津")
.put("13", "河北")
.put("14", "山西")
.put("15", "内蒙古")
.put("21", "辽宁")
.put("22", "吉林")
.put("23", "黑龙江")
.put("31", "上海")
.put("32", "江苏")
.put("33", "浙江")
.put("34", "安徽")
.put("35", "福建")
.put("36", "江西")
.put("37", "山东")
.put("41", "河南")
.put("42", "湖北")
.put("43", "湖南")
.put("44", "广东")
.put("45", "广西")
.put("46", "海南")
.put("50", "重庆")
.put("51", "四川")
.put("52", "贵州")
.put("53", "云南")
.put("54", "西藏")
.put("61", "陕西")
.put("62", "甘肃")
.put("63", "青海")
.put("64", "宁夏")
.put("65", "新疆")
.put("71", "台湾")
.put("81", "香港")
.put("82", "澳门")
.put("83", "台湾") // 台湾身份证号码以83开头但是行政区划为71
.put("91", "国外")
.build();
@SuppressWarnings("null")
@Override
public int compareTo(Chinese2ndGenIDCardNumber o) {
return value.compareTo(o.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Chinese2ndGenIDCardNumber)) {
return false;
}
Chinese2ndGenIDCardNumber other = (Chinese2ndGenIDCardNumber) obj;
return Objects.equals(value, other.value);
}
@Override
public String toString() {
return value();
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkCondition;
import xyz.zhouxy.plusone.commons.base.IWithIntCode;
/**
* 性别
*
* @author ZhouXY
*/
public enum Gender implements IWithIntCode {
UNKNOWN(0, "Unknown", "未知"),
MALE(1, "Male", ""),
FEMALE(2, "Female", ""),
;
private static final Gender[] VALUES = new Gender[] { UNKNOWN, MALE, FEMALE };
private final int value;
private final String displayName;
private final String displayNameZh;
Gender(int value, String displayName, String displayNameZh) {
this.value = value;
this.displayName = displayName;
this.displayNameZh = displayNameZh;
}
/**
* 根据码值获取对应枚举
*
* @param value 码值
* @return 枚举值
*/
public static Gender of(int value) {
checkCondition(0 <= value && value < VALUES.length,
() -> new EnumConstantNotPresentException(Gender.class, String.valueOf(value)));
return VALUES[value];
}
/**
* 获取枚举码值
*
* @return 码值
*/
public int getValue() {
return value;
}
/**
* 枚举名称
*
* @return 枚举名称
*/
public String getDisplayName() {
return displayName;
}
/**
* 枚举中文名称
*
* @return 枚举中文名称
*/
public String getDisplayNameZh() {
return displayNameZh;
}
@Override
public int getCode() {
return getValue();
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model;
import java.time.LocalDate;
import java.time.Period;
import xyz.zhouxy.plusone.commons.util.StringTools;
/**
* 身份证号
*
* @author ZhouXY
*/
public interface IDCardNumber {
char DEFAULT_REPLACED_CHAR = '*';
int DEFAULT_DISPLAY_FRONT = 1;
int DEFAULT_DISPLAY_END = 2;
/**
* 身份证号
*
* @return 身份证号
*/
String value();
/**
* 根据身份证号判断性别
*
* @return {@link Gender} 对象
*/
Gender getGender();
/**
* 获取出生日期
*
* @return 出生日期
*/
LocalDate getBirthDate();
/**
* 计算年龄
*
* @return 年龄
*/
default int getAge() {
LocalDate now = LocalDate.now();
return Period.between(getBirthDate(), now).getYears();
}
// ================================
// #region - toString
// ================================
/**
* 脱敏字符串(前面留 1 位,后面留 2 位)
*
* @return 脱敏字符串
*/
default String toDesensitizedString() {
return StringTools.desensitize(value(), DEFAULT_REPLACED_CHAR, DEFAULT_DISPLAY_FRONT, DEFAULT_DISPLAY_END);
}
/**
* 脱敏字符串
*
* @param front 前面保留的字符数
* @param end 后面保留的字符数
* @return 脱敏字符串
*/
default String toDesensitizedString(int front, int end) {
return StringTools.desensitize(value(), DEFAULT_REPLACED_CHAR, front, end);
}
/**
* 脱敏字符串
*
* @param replacedChar 替换字符
* @param front 前面保留的字符数
* @param end 后面保留的字符数
* @return 脱敏字符串
*/
default String toDesensitizedString(char replacedChar, int front, int end) {
return StringTools.desensitize(value(), replacedChar, front, end);
}
// ================================
// #endregion - toString
// ================================
}

View File

@@ -0,0 +1,275 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import java.io.Serializable;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import com.google.common.base.Splitter;
import xyz.zhouxy.plusone.commons.util.StringTools;
/**
* SemVer 语义版本号
*
* @author ZhouXY
* @since 1.1.0
*
* @see <a href="https://semver.org/">Semantic Versioning 2.0.0</a>
*/
public class SemVer implements Comparable<SemVer>, Serializable {
private static final long serialVersionUID = 458265121025514002L;
private final String value;
private final int[] versionNumbers;
@Nullable
private final String preReleaseVersion;
@Nullable
private final String buildMetadata;
private static final String VERSION_NUMBERS = "(?<numbers>(?<major>0|[1-9]\\d*)\\.(?<minor>0|[1-9]\\d*)\\.(?<patch>0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){0,2})";
private static final String PRE_RELEASE_VERSION = "(?:-(?<prerelease>(?:0|[1-9]\\d{0,41}|\\d{0,18}[a-zA-Z-][0-9a-zA-Z-]{0,18})(?:\\.(?:0|[1-9]\\d{0,41}|\\d{0,18}[a-zA-Z-][0-9a-zA-Z-]{0,18})){0,18}))?";
private static final String BUILD_METADATA = "(?:\\+(?<buildmetadata>[0-9a-zA-Z-]{1,18}(?:\\.[0-9a-zA-Z-]{1,18}){0,18}))?";
private static final Pattern PATTERN = Pattern.compile(
"^" + VERSION_NUMBERS + PRE_RELEASE_VERSION + BUILD_METADATA + "$");
/**
* 创建语义化版本号的值对象
*
* @param value 字符串值
* @param versionNumbers 主版本号、次版本号、修订号
* @param preReleaseVersion 先行版本号
* @param buildMetadata 版本编译信息
*/
private SemVer(String value,
int[] versionNumbers,
@Nullable String preReleaseVersion,
@Nullable String buildMetadata) {
this.value = value;
this.versionNumbers = versionNumbers;
this.preReleaseVersion = preReleaseVersion;
this.buildMetadata = buildMetadata;
}
/**
* 创建 SemVer 对象
*
* @param value 语义化版本号
* @return SemVer 对象
*/
public static SemVer of(final String value) {
checkArgument(StringTools.isNotBlank(value), "版本号不能为空");
final Matcher matcher = PATTERN.matcher(value);
checkArgument(matcher.matches(), "版本号格式错误");
// 数字版本部分
final String versionNumbersPart = matcher.group("numbers");
// 先行版本号部分
final String preReleaseVersionPart = matcher.group("prerelease");
// 版本编译信息部分
final String buildMetadataPart = matcher.group("buildmetadata");
final int[] versionNumbers = Splitter.on('.')
.splitToStream(versionNumbersPart)
// 必须都是数字
.mapToInt(Integer::parseInt)
.toArray();
return new SemVer(value, versionNumbers, preReleaseVersionPart, buildMetadataPart);
}
/**
* 获取主版本号
*
* @return 主版本号
*/
public int getMajor() {
return this.versionNumbers[0];
}
/**
* 获取次版本号
*
* @return 次版本号
*/
public int getMinor() {
return this.versionNumbers[1];
}
/**
* 获取修订号
*
* @return 修订号
*/
public int getPatch() {
return this.versionNumbers[2];
}
/**
* 获取先行版本号
*
* @return 先行版本号
*/
@Nullable
public String getPreReleaseVersion() {
return this.preReleaseVersion;
}
/**
* 获取版本编译信息
*
* @return 版本编译信息
*/
@Nullable
public String getBuildMetadata() {
return buildMetadata;
}
/** {@inheritDoc} */
@Override
public int compareTo(@SuppressWarnings("null") SemVer that) {
if (this == that) {
return 0;
}
int result = compareVersionNumbers(that);
if (result != 0) {
return result;
}
return comparePreReleaseVersion(that);
}
/**
* 获取字符串值
*
* @return 版本字符串
*/
public String getValue() {
return value;
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return Objects.hash(value);
}
/** {@inheritDoc} */
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj)
return true;
if (!(obj instanceof SemVer))
return false;
SemVer other = (SemVer) obj;
return Objects.equals(value, other.value);
}
/**
* 获取 SemVer 的字符串表示。如 {@code v1.2.3-alpha.1+build.1234}
*/
@Override
public String toString() {
return 'v' + value;
}
/**
* 比较主版本号、次版本号、修订号
*/
private int compareVersionNumbers(SemVer that) {
final int minLength = Integer.min(this.versionNumbers.length, that.versionNumbers.length);
for (int i = 0; i < minLength; i++) {
final int currentVersionNumberOfThis = this.versionNumbers[i];
final int currentVersionNumberOfThat = that.versionNumbers[i];
if (currentVersionNumberOfThis != currentVersionNumberOfThat) {
return currentVersionNumberOfThis - currentVersionNumberOfThat;
}
}
return this.versionNumbers.length - that.versionNumbers.length;
}
/**
* 比较先行版本号
*/
private int comparePreReleaseVersion(SemVer that) {
int thisWithoutPreReleaseVersionFlag = bool2Int(this.preReleaseVersion == null);
int thatWithoutPreReleaseVersionFlag = bool2Int(that.preReleaseVersion == null);
if (isTrue(thisWithoutPreReleaseVersionFlag | thatWithoutPreReleaseVersionFlag)) {
return thisWithoutPreReleaseVersionFlag - thatWithoutPreReleaseVersionFlag;
}
Splitter splitter = Splitter.on('.');
final String[] preReleaseVersionOfThis = splitter
.splitToStream(this.preReleaseVersion) // NOSONAR
.toArray(String[]::new);
final String[] preReleaseVersionOfThat = splitter
.splitToStream(that.preReleaseVersion) // NOSONAR
.toArray(String[]::new);
final int minLength = Integer.min(preReleaseVersionOfThis.length, preReleaseVersionOfThat.length);
for (int i = 0; i < minLength; i++) {
int r = comparePartOfPreReleaseVersion(preReleaseVersionOfThis[i], preReleaseVersionOfThat[i]);
if (r != 0) {
return r;
}
}
return preReleaseVersionOfThis.length - preReleaseVersionOfThat.length;
}
/**
* 比较先行版本号的组成部分
*/
private static int comparePartOfPreReleaseVersion(String p1, String p2) {
boolean p1IsNumber = isAllDigits(p1);
boolean p2IsNumber = isAllDigits(p2);
if (p1IsNumber) {
return p2IsNumber
? Integer.parseInt(p1) - Integer.parseInt(p2) // 都是数字
: -1; // p1 是数字p2 是字符串
}
// 如果 p1 是字符串p2 是数字,则返回 1字符串优先于纯数字
return p2IsNumber ? 1 : p1.compareTo(p2);
}
/**
* 判断字符串是否全为数字
*/
private static boolean isAllDigits(String str) {
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c < '0' || c > '9') {
return false;
}
}
return true;
}
private static int bool2Int(boolean expression) {
return expression ? 1 : 0;
}
private static boolean isTrue(int b) {
return b != 0;
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.annotation.ReaderMethod;
/**
* 带校验的字符串值对象
*
* @author ZhouXY
* @since 1.0.0
*
* @deprecated 弃用。使用工厂方法创建对象,并在其中进行校验即可。
*/
@Deprecated
public abstract class ValidatableStringRecord<T extends ValidatableStringRecord<T>> // NOSONAR 暂不删除
implements Comparable<T> {
@Nonnull
private final String value;
private final Matcher matcher;
/**
* 构造字符串值对象
*
* @param value 字符串值
* @param pattern 正则
*/
protected ValidatableStringRecord(String value, Pattern pattern) {
this(value, pattern, "Invalid value");
}
/**
* 构造字符串值对象
*
* @param value 字符串值
* @param pattern 正则
* @param errorMessageSupplier 正则不匹配时的错误信息
*/
protected ValidatableStringRecord(String value, Pattern pattern,
Supplier<String> errorMessageSupplier) {
this(value, pattern, errorMessageSupplier.get());
}
/**
* 构造字符串值对象
*
* @param value 字符串值
* @param pattern 正则
* @param errorMessage 正则不匹配时的错误信息
*/
protected ValidatableStringRecord(String value, Pattern pattern, String errorMessage) {
checkArgumentNotNull(value, "The value cannot be null.");
checkArgumentNotNull(pattern, "The pattern cannot be null.");
this.matcher = pattern.matcher(value);
checkArgument(this.matcher.matches(), errorMessage);
this.value = value;
}
/**
* 值对象的字符串值。
*
* @return 字符串(不为空)
*/
@ReaderMethod
public final String value() {
return this.value;
}
@Override
public int compareTo(@SuppressWarnings("null") T o) {
return this.value.compareTo(o.value());
}
@Override
public int hashCode() {
return Objects.hash(getClass(), value);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
@SuppressWarnings("unchecked")
T other = (T) obj;
return Objects.equals(value, other.value());
}
@Override
public String toString() {
return this.value();
}
/**
* 获取正则匹配结果
*
* @return {@code Matcher} 对象
*/
protected final Matcher getMatcher() {
return matcher;
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model.dto;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
import xyz.zhouxy.plusone.commons.collection.CollectionTools;
/**
* 返回分页查询的结果
*
* @param <T> 内容列表的元素类型
*
* @author ZhouXY
* @see PagingAndSortingQueryParams
*/
public class PageResult<T> {
private final long total;
private final List<T> content;
private PageResult(@Nullable final List<T> content, final long total) {
this.content = CollectionTools.nullToEmptyList(content);
this.total = total;
}
/**
* 创建一个分页查询的结果
*
* @param <T> 内容类型
* @param content 一页数据
* @param total 总数据量
* @return 分页查询的结果
*/
@StaticFactoryMethod(PageResult.class)
public static <T> PageResult<T> of(@Nullable final List<T> content, final long total) {
return new PageResult<>(content, total);
}
/**
* 创建一个空的分页查询的结果
*
* @param <T> 内容类型
* @return 空结果
*/
@StaticFactoryMethod(PageResult.class)
public static <T> PageResult<T> empty() {
return new PageResult<>(Collections.emptyList(), 0L);
}
/**
* 总数据量
*
* @return 总数据量
*/
public long getTotal() {
return total;
}
/**
* 一页数据
*
* @return 一页数据
*/
public List<T> getContent() {
return content;
}
@Override
public String toString() {
return "PageResult [total=" + total + ", content=" + content + "]";
}
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright 2022-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model.dto;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableMap;
import xyz.zhouxy.plusone.commons.collection.CollectionTools;
import xyz.zhouxy.plusone.commons.util.RegexTools;
import xyz.zhouxy.plusone.commons.util.StringTools;
/**
* 分页排序查询参数
*
* <p>
* 包含三个主要的属性:
* <ul>
* <li>size - 每页显示的记录数</li>
* <li>pageNum - 当前页码</li>
* <li>orderBy - 排序条件</li>
* </ul>
*
* <p>
* 分页必须伴随着排序,不然可能出现同一个对象重复出现在不同页,有的对象不被查询到的情况。
*
* <p>
* 其中 {@code orderBy} 是一个 {@code List&lt;String&gt;},可以指定多个排序条件。
* 每个排序条件是一个字符串, 格式为“属性名-ASC”或“属性名-DESC”分别表示升序和降序。
* 例如,当 {@code orderBy} 的值为 {@code ["name-ASC","age-DESC"]}
* 意味着要按 {@code name} 进行升序排列,{@code name} 相同的情况下则按 {@code age} 进行降序排列。
*
* <p>
* 用户可继承 {@link PagingAndSortingQueryParams} 构建自己的分页查询入参,
* 子类需在构造器中调用 {@link PagingAndSortingQueryParams} 的构造器,
* 传入一个 {@link PagingParamsBuilder} 用于构建分页参数。
* 同一场景下,复用一个 {@link PagingParamsBuilder} 实例即可。
*
* <p>
* 构建 {@link PagingParamsBuilder} 时,需传入一个 {@code Map} 作为可排序字段的白名单,
* {@code key} 是供前端指定用于排序的属性名,{@code value} 是对应数据库中的字段名。
* 只有在此白名单中的属性名才允许用于排序。
*
* <pre>
* class AccountQueryParams extends PagingAndSortingQueryParams {
* private static final Map&lt;String, String&gt; PROPERTY_COLUMN_MAP = ImmutableMap.&lt;String, String&gt;builder()
* .put("id", "id")
* .put("username", "username")
* .build();
* private static final PagingParamsBuilder PAGING_PARAMS_BUILDER = PagingAndSortingQueryParams
* .pagingParamsBuilder(20, 100, PROPERTY_COLUMN_MAP);
*
* public AccountQueryParams() {
* // 所有的 AccountQueryParams 复用同一个 PagingParamsBuilder 实例
* super(PAGING_PARAMS_BUILDER);
* }
*
* private @Getter @Setter Long id;
* private @Getter @Setter String username;
* private @Getter @Setter String email;
* private @Getter @Setter Integer status;
* }
*
* public PageResult&lt;AccountVO&gt; queryPage(AccountQueryParams params) {
* // 获取分页参数
* PagingParams pagingParams = params.buildPagingParams();
* // 从 params 获取字段查询条件,从 pagingParams 获取分页条件,查询一页数据
* List&lt;AccountVO&gt; list = accountQueries.queryAccountList(params, pagingParams);
* // 查询总记录数
* long count = accountQueries.countAccount(params);
* // 返回分页结果
* return PageResult.of(list, count);
* }
* </pre>
*
* @author ZhouXY
* @see PagingParams
* @see PageResult
*/
public class PagingAndSortingQueryParams {
private final PagingParamsBuilder pagingParamsBuilder;
private Integer size;
private Long pageNum;
private List<String> orderBy;
/**
* 创建一个 {@code PagingAndSortingQueryParams} 实例
*
* @param pagingParamsBuilder
* 分页参数构造器。
* 通过 {@link #pagingParamsBuilder(int, int, Map)} 创建,同一场景下只需要共享同一个实例。
*/
protected PagingAndSortingQueryParams(PagingParamsBuilder pagingParamsBuilder) {
this.pagingParamsBuilder = pagingParamsBuilder;
}
// Setters
/**
* 设置排序规则
*
* @param orderBy 排序规则,不能为空
*/
public final void setOrderBy(List<String> orderBy) {
this.orderBy = orderBy;
}
/**
* 设置每页大小
*
* @param size 每页大小
*/
public final void setSize(@Nullable Integer size) {
this.size = size;
}
/**
* 设置页码
*
* @param pageNum 页码
*/
public final void setPageNum(@Nullable Long pageNum) {
this.pageNum = pageNum;
}
// Setters end
@Override
public String toString() {
return "PagingAndSortingQueryParams ["
+ "size=" + size
+ ", pageNum=" + pageNum
+ ", orderBy=" + orderBy
+ "]";
}
/**
* 创建一个分页参数构造器
*
* @param defaultSize 默认每页大小
* @param maxSize 最大每页大小
* @param sortableProperties
* 可排序属性。
* key 是供前端指定用于排序的属性名value 是对应数据库中的字段名。
* 只有在此白名单中的属性名才允许用于排序。
* @return 分页参数构造器
*/
protected static PagingParamsBuilder pagingParamsBuilder(
int defaultSize, int maxSize, Map<String, String> sortableProperties) {
return new PagingParamsBuilder(defaultSize, maxSize, sortableProperties);
}
/**
* 根据当前查询参数,构建分页参数
*
* @return 分页参数
*/
public PagingParams buildPagingParams() {
return this.pagingParamsBuilder.buildPagingParams(this);
}
/**
* 可排序属性
*/
public static final class SortableProperty {
private final String propertyName;
private final String columnName;
private final String orderType;
private final String sqlSnippet;
private SortableProperty(String propertyName, String columnName, String orderType) {
this.propertyName = propertyName;
this.columnName = columnName;
checkArgument("ASC".equalsIgnoreCase(orderType) || "DESC".equalsIgnoreCase(orderType));
this.orderType = orderType.toUpperCase();
this.sqlSnippet = this.propertyName + " " + this.orderType;
}
/**
* 属性名
*
* @return 属性名
*/
public String getPropertyName() {
return propertyName;
}
/**
* 对应数据库中列名称
*
* @return 列名称
*/
public String getColumnName() {
return columnName;
}
/**
* 排序方式
*
* @return 排序方式
*/
public String getOrderType() {
return orderType;
}
/**
* SQL 片段
*
* @return SQL 片段
*/
public String getSqlSnippet() {
return sqlSnippet;
}
@Override
public String toString() {
return "SortableProperty ["
+ "propertyName=" + propertyName
+ ", columnName=" + columnName
+ ", orderType=" + orderType
+ "]";
}
}
protected static final class PagingParamsBuilder {
private static final Pattern SORT_STR_PATTERN = Pattern.compile("^[a-zA-Z][\\w-]{0,63}-(desc|asc|DESC|ASC)$");
private final Map<String, String> sortableProperties;
protected final int defaultSize;
protected final int maxSize;
private PagingParamsBuilder(int defaultSize, int maxSize, Map<String, String> sortableProperties) {
this.defaultSize = defaultSize;
this.maxSize = maxSize;
checkArgument(CollectionTools.isNotEmpty(sortableProperties),
"Sortable properties can not be empty.");
sortableProperties.forEach((k, v) ->
checkArgument(StringTools.isNotBlank(k) && StringTools.isNotBlank(v),
"Property name must not be blank."));
this.sortableProperties = ImmutableMap.copyOf(sortableProperties);
}
public PagingParams buildPagingParams(PagingAndSortingQueryParams params) {
final int sizeValue = params.size != null ? params.size : this.defaultSize;
final long pageNumValue = params.pageNum != null ? params.pageNum : 1L;
checkArgument(CollectionTools.isNotEmpty(params.orderBy),
"The 'orderBy' cannot be empty");
final List<SortableProperty> propertiesToSort = params.orderBy.stream()
.map(this::generateSortableProperty)
.collect(Collectors.toList());
return new PagingParams(sizeValue, pageNumValue, propertiesToSort);
}
private SortableProperty generateSortableProperty(String orderByStr) {
checkArgument(StringTools.isNotBlank(orderByStr));
checkArgument(RegexTools.matches(orderByStr, SORT_STR_PATTERN));
String[] propertyNameAndOrderType = orderByStr.split("-");
checkArgument(propertyNameAndOrderType.length == 2);
String propertyName = propertyNameAndOrderType[0];
checkArgument(sortableProperties.containsKey(propertyName),
"The property name must be in the set of sortable properties.");
String columnName = sortableProperties.get(propertyName);
String orderType = propertyNameAndOrderType[1];
return new SortableProperty(propertyName, columnName, orderType);
}
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model.dto;
import java.util.Collections;
import java.util.List;
import xyz.zhouxy.plusone.commons.model.dto.PagingAndSortingQueryParams.SortableProperty;
/**
* 分页参数
*
* @author ZhouXY
* @see PagingAndSortingQueryParams
*/
public class PagingParams {
/** 每页大小 */
private final int size;
/** 当前页码 */
private final long pageNum;
/** 偏移量 */
private final long offset;
/** 排序 */
private final List<SortableProperty> orderBy;
PagingParams(int size, long pageNum, List<SortableProperty> orderBy) {
this.size = size;
this.pageNum = pageNum;
this.offset = (pageNum - 1) * size;
this.orderBy = orderBy;
}
// Getters
/**
* 排序规则
*
* @return 排序规则
*/
public final List<SortableProperty> getOrderBy() {
return Collections.unmodifiableList(this.orderBy);
}
/**
* 每页大小
*
* @return 每页大小
*/
public final int getSize() {
return this.size;
}
/**
* 当前页码
*
* @return 当前页码
*/
public final long getPageNum() {
return this.pageNum;
}
/**
* 偏移量
*
* @return 偏移量
*/
public final long getOffset() {
return this.offset;
}
// Getters end
@Override
public String toString() {
return "PageInfo [size=" + size + ", pageNum=" + pageNum + ", orderBy=" + orderBy + ", offset="
+ getOffset() + "]";
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model.dto;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* 统一结果,对返回给前端的数据进行封装。
*
* @author ZhouXY
*/
public class UnifiedResponse<T> {
private final String code;
private final String message;
private final @Nullable T data;
// ================================
// #region - Constructors
// ================================
/**
* 构造 {@code UnifiedResponse}
*
* @param code 状态码
* @param message 响应信息
*/
UnifiedResponse(String code, @Nullable String message) {
this(code, message, null);
}
/**
* 构造 {@code UnifiedResponse}
*
* @param code 状态码
* @param message 响应信息
* @param data 响应数据
*/
UnifiedResponse(String code, @Nullable String message, @Nullable T data) {
this.code = Objects.requireNonNull(code);
this.message = message == null ? "" : message;
this.data = data;
}
// ================================
// #endregion - Constructors
// ================================
// ================================
// #region - Getters
// ================================
/**
* 状态码
*
* @return 状态码
*/
public String getCode() {
return code;
}
/**
* 响应信息
*
* @return 响应信息
*/
public String getMessage() {
return message;
}
/**
* 响应数据
*
* @return 响应数据
*/
@Nullable
public T getData() {
return data;
}
// ================================
// #endregion - Getters
// ================================
@Override
public String toString() {
return String.format("{code: \"%s\", message: \"%s\", data: %s}",
this.code, this.message, transValue(this.data));
}
private static String transValue(@Nullable Object value) {
if (value == null) {
return null;
}
if (value instanceof String) {
return "\"" + value + "\"";
}
return String.valueOf(value);
}
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.model.dto;
import javax.annotation.Nullable;
/**
* {@link UnifiedResponse} 工厂类。
* 用于快速构建 {@link UnifiedResponse} 对象,默认的成功代码为 {@code 2000000}。
*
* <p>
* 用户可以继承 {@link UnifiedResponses} 实现自己的工厂类,
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG以及工厂方法。
* 如下所示:
* <pre>
* // 自定义工厂类
* public static class CustomUnifiedResponses extends UnifiedResponses {
*
* public static final String SUCCESS_CODE = "000";
* public static final String DEFAULT_SUCCESS_MSG = "成功";
*
* public static &lt;T&gt; UnifiedResponse&lt;T&gt; success() {
* return of(SUCCESS_CODE, DEFAULT_SUCCESS_MSG);
* }
*
* public static &lt;T&gt; UnifiedResponse&lt;T&gt; success(@Nullable String message) {
* return of(SUCCESS_CODE, message);
* }
*
* public static &lt;T&gt; UnifiedResponse&lt;T&gt; success(@Nullable String message, @Nullable T data) {
* return of(SUCCESS_CODE, message, data);
* }
*
* private CustomUnifiedResponses() {
* super();
* }
* }
* // 使用自定义工厂类
* CustomUnifiedResponses.success("查询成功", userList); // 状态码为 000
* </pre>
* 见 <a href="http://zhouxy.xyz:3000/plusone/plusone-commons/issues/22">issue#22</a>。
*
* @author ZhouXY
* @since 1.0.0
* @see UnifiedResponse
*/
public class UnifiedResponses {
public static final String SUCCESS_CODE = "2000000";
public static final String DEFAULT_SUCCESS_MSG = "SUCCESS";
// ================================
// #region - success
// ================================
/**
* 默认成功响应结果
*
* @param <T> data 类型
* @return {@code UnifiedResponse} 对象。
* {@code code} = "2000000", {@code message} = "SUCCESS", {@code data} = null
*/
public static <T> UnifiedResponse<T> success() {
return new UnifiedResponse<>(SUCCESS_CODE, DEFAULT_SUCCESS_MSG);
}
/**
* 使用指定 {@code message} 创建成功响应结果
*
* @param message 成功信息
* @param <T> data 类型
* @return {@code UnifiedResponse} 对象。
* {@code code} = "2000000", {@code data} = null
*/
public static <T> UnifiedResponse<T> success(@Nullable String message) {
return new UnifiedResponse<>(SUCCESS_CODE, message);
}
/**
* 使用指定 {@code message} 和 {@code data} 创建成功响应结果
*
* @param <T> data 类型
* @param message 成功信息
* @param data 携带数据
* @return {@code UnifiedResponse} 对象。
* {@code code} = "2000000"
*/
public static <T> UnifiedResponse<T> success(@Nullable String message, @Nullable T data) {
return new UnifiedResponse<>(SUCCESS_CODE, message, data);
}
// ================================
// #endregion - success
// ================================
// ================================
// #region - error
// ================================
/**
* 创建错误响应结果
*
* @param code 错误码
* @param message 错误信息
* @param <T> data 类型
* @return {@code UnifiedResponse} 对象({@code data} 为 {@code null}
*/
public static <T> UnifiedResponse<T> error(String code, @Nullable String message) {
return new UnifiedResponse<>(code, message);
}
/**
* 创建错误响应结果
*
* @param <T> data 类型
* @param code 错误码
* @param message 错误信息
* @param data 携带数据
* @return {@code UnifiedResponse} 对象
*/
public static <T> UnifiedResponse<T> error(String code, @Nullable String message, @Nullable T data) {
return new UnifiedResponse<>(code, message, data);
}
/**
* 创建错误响应结果
*
* @param code 错误码
* @param e 异常
* @return {@code UnifiedResponse} 对象。
* {@code message} 为异常的 {@code message}
* {@code data} 为 {@code null}。
*/
public static <T> UnifiedResponse<T> error(String code, Throwable e) {
return new UnifiedResponse<>(code, e.getMessage());
}
// ================================
// #endregion - error
// ================================
// ================================
// #region - of
// ================================
/**
* 创建响应结果
*
* @param code 状态码
* @param message 响应信息
* @param <T> data 类型
* @return {@code UnifiedResponse} 对象({@code data} 为 {@code null}
*/
public static <T> UnifiedResponse<T> of(String code, @Nullable String message) {
return new UnifiedResponse<>(code, message);
}
/**
* 创建响应结果
*
* @param <T> data 类型
* @param code 状态码
* @param message 响应信息
* @param data 携带数据
* @return {@code UnifiedResponse} 对象
*/
public static <T> UnifiedResponse<T> of(String code, @Nullable String message, @Nullable T data) {
return new UnifiedResponse<>(code, message, data);
}
// ================================
// #endregion - of
// ================================
protected UnifiedResponses() {
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* <h2>数据传输对象</h2>
*
* <h3>1. 分页</h3>
* <p>
* 分页组件由 {@link PagingAndSortingQueryParams} 作为入参,
* 因为分页必须伴随着排序,不然可能出现同一个对象重复出现在不同页,有的对象不被查询到的情况,
* 所以分页查询的入参必须包含排序条件。
*
* <p>
* 用户可继承 {@link PagingAndSortingQueryParams}
* 构建自己的分页查询入参,需在构造器中调用 {@link PagingAndSortingQueryParams} 的构造器,传入一个 Map 作为白名单,
* key 是供前端指定用于排序的属性名value 是对应数据库中的字段名,只有在白名单中指定的属性名才允许作为排序条件。
*
* <p>
* {@link PagingAndSortingQueryParams} 包含三个主要的属性:
* <ul>
* <li>size - 每页显示的记录数</li>
* <li>pageNum - 当前页码</li>
* <li>orderBy - 排序条件</li>
* </ul>
* 其中 orderBy 是一个 List可以指定多个排序条件每个排序条件是一个字符串
* 格式为“属性名-ASC”或“属性名-DESC”分别表示升序和降序。
*
* <p>
* 比如前端传入的 orderBy 为 ["name-ASC","age-DESC"],意味着要按 name 进行升序name 相同的情况下则按 age 进行降序。
*
* <p>
* 使用时调用 {@link PagingAndSortingQueryParams#buildPagingParams()} 方法获取分页参数 {@link PagingParams}。
*
* <p>
* 分页结果可以存放到 {@link PageResult} 中,作为出参。
*
* <h3>2. {@link UnifiedResponse}</h3>
* <p>
* {@link UnifiedResponse} 对返回给前端的数据进行封装,包含 code、message、data。
*
* <p>
* {@link UnifiedResponses} 用于快速构建 {@link UnifiedResponse} 对象,默认的成功代码为 {@code 2000000}。
*
* <p>
* 用户可以继承 {@link UnifiedResponses} 实现自己的工厂类,
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG以及工厂方法。
*
* @author ZhouXY
*/
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.model.dto;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,22 +14,14 @@
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.function;
import java.util.OptionalLong;
import java.util.function.Supplier;
/**
* OptionalLongSupplier
*
* <h2>业务建模组件</h2>
* <p>
* 返回 {@link OptionalLong} 对象
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @see OptionalLong
* @see Supplier
* 包含业务建模可能用到的性别身份证等元素也包含 DTO 相关类如分页查询参数响应结果分页结果等
*
* @author ZhouXY
*/
@FunctionalInterface
public interface OptionalLongSupplier extends Supplier<OptionalLong> {
}
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.model;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -0,0 +1,273 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.time;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkNotNull;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkCondition;
import java.time.DateTimeException;
import java.time.Month;
import java.time.MonthDay;
import java.time.temporal.ChronoField;
import com.google.common.collect.Range;
import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
import xyz.zhouxy.plusone.commons.base.IWithIntCode;
/**
* 季度
*
* @author ZhouXY
*/
public enum Quarter implements IWithIntCode {
/** 第一季度 */
Q1(1),
/** 第二季度 */
Q2(2),
/** 第三季度 */
Q3(3),
/** 第四季度 */
Q4(4),
;
/** 季度值 (1/2/3/4) */
private final int value;
/** 月份范围 */
private final Range<Integer> monthRange;
/** 常量值 */
private static final Quarter[] ENUMS = Quarter.values();
/**
* @param value 季度值 (1/2/3/4)
*/
Quarter(int value) {
this.value = value;
final int lastMonth = value * 3;
final int firstMonth = lastMonth - 2;
this.monthRange = Range.closed(firstMonth, lastMonth);
}
// ================================
// #region - StaticFactoryMethods
// ================================
/**
* 根据给定的月份值返回对应的季度
*
* @param monthValue 月份值取值范围为1到12
* @return 对应的季度
* @throws IllegalArgumentException 如果月份值不在有效范围内1到12将抛出异常
*/
@StaticFactoryMethod(Quarter.class)
public static Quarter fromMonth(int monthValue) {
ChronoField.MONTH_OF_YEAR.checkValidValue(monthValue);
return of(computeQuarterValueInternal(monthValue));
}
/**
* 根据给定的月份返回对应的季度
*
* @param month 月份
* @return 对应的季度
*/
@StaticFactoryMethod(Quarter.class)
public static Quarter fromMonth(Month month) {
checkNotNull(month);
final int monthValue = month.getValue();
return of(computeQuarterValueInternal(monthValue));
}
/**
* 根据指定的年份,获取一个新的 YearQuarter 实例
* 此方法允许在保持当前季度信息不变的情况下,更改年份
*
* @param year 指定的年份
* @return 返回一个新的 YearQuarter 实例,年份更新为指定的年份
*/
public final YearQuarter atYear(int year) {
return YearQuarter.of(year, this);
}
/**
* 根据给定的季度值返回对应的季度
*
* @param value 季度值 (1/2/3/4)
* @return 对应的季度
* @throws IllegalArgumentException 如果季度值不在有效范围内1到4将抛出异常
*/
@StaticFactoryMethod(Quarter.class)
public static Quarter of(int value) {
return ENUMS[checkValidIntValue(value) - 1];
}
// ================================
// #endregion - StaticFactoryMethods
// ================================
// ================================
// #region - computes
// ================================
/**
* 加上指定数量的季度
*
* @param quarters 所添加的季度数量
* @return 计算结果
*/
public Quarter plus(long quarters) {
final int amount = (int) ((quarters % 4) + 4);
return ENUMS[(ordinal() + amount) % 4];
}
/**
* 减去指定数量的季度
*
* @param quarters 所减去的季度数量
* @return 计算结果
*/
public Quarter minus(long quarters) {
return plus(-(quarters % 4));
}
// ================================
// #endregion - computes
// ================================
// ================================
// #region - Getters
// ================================
/**
* 获取季度值
*
* @return 季度值
*/
public int getValue() {
return value;
}
@Override
public int getCode() {
return getValue();
}
/**
* 该季度的第一个月
*
* @return {@code Month} 对象
*/
public Month firstMonth() {
return Month.of(firstMonthValue());
}
/**
* 该季度的第一个月
*
* @return 月份值从 1 开始1 表示 1月以此类推。
*/
public int firstMonthValue() {
return this.monthRange.lowerEndpoint();
}
/**
* 该季度的最后一个月
*
* @return {@code Month} 对象
*/
public Month lastMonth() {
return Month.of(lastMonthValue());
}
/**
* 该季度的最后一个月
*
* @return 月份值从 1 开始1 表示 1月以此类推。
*/
public int lastMonthValue() {
return this.monthRange.upperEndpoint();
}
/**
* 该季度的第一天
*
* @return {@code MonthDay} 对象
*/
public MonthDay firstMonthDay() {
return MonthDay.of(firstMonth(), 1);
}
/**
* 该季度的最后一天
*
* @return {@code MonthDay} 对象
*/
public MonthDay lastMonthDay() {
// 季度的最后一个月不可能是 2 月
final Month month = lastMonth();
return MonthDay.of(month, month.maxLength());
}
/**
* 计算该季度的第一天为当年的第几天
*
* @param leapYear 是否为闰年
* @return day of year
*/
public int firstDayOfYear(boolean leapYear) {
return firstMonth().firstDayOfYear(leapYear);
}
// ================================
// #endregion - Getters
// ================================
/**
* 检查给定的季度值是否有效
*
* @param value 季度值
* @return 如果给定的季度值有效则返回该值
* @throws DateTimeException 如果给定的季度值不在有效范围内1到4将抛出异常
*/
public static int checkValidIntValue(int value) {
checkCondition(value >= 1 && value <= 4,
() -> new DateTimeException("Invalid value for Quarter: " + value));
return value;
}
// ================================
// #region - Internal
// ================================
/**
* 计算给定月份对应的季度值
*
* @param monthValue 月份值取值范围为1到12
* @return 对应的季度值
*/
private static int computeQuarterValueInternal(int monthValue) {
return (monthValue - 1) / 3 + 1;
}
// ================================
// #endregion - Internal
// ================================
}

View File

@@ -0,0 +1,393 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.time;
import static java.time.temporal.ChronoField.YEAR;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkNotNull;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.Month;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
import java.util.TimeZone;
import javax.annotation.Nullable;
import com.google.errorprone.annotations.Immutable;
import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
/**
* 表示年份与季度
*
* @author ZhouXY
*/
@Immutable
public final class YearQuarter implements Comparable<YearQuarter>, Serializable {
private static final long serialVersionUID = 3804145964419489753L;
/** 年份 */
private final int year;
/** 季度 */
private final Quarter quarter;
/** 季度开始日期 */
private final LocalDate firstDate;
/** 季度结束日期 */
private final LocalDate lastDate;
private YearQuarter(int year, Quarter quarter) {
this.year = year;
this.quarter = quarter;
this.firstDate = quarter.firstMonthDay().atYear(year);
this.lastDate = quarter.lastMonthDay().atYear(year);
}
// #region - StaticFactory
/**
* 根据指定年份与季度,创建 {@link YearQuarter} 实例
*
* @param year 年份
* @param quarter 季度
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter of(int year, int quarter) {
return new YearQuarter(YEAR.checkValidIntValue(year), Quarter.of(quarter));
}
/**
* 根据指定年份与季度,创建 {@link YearQuarter} 实例
*
* @param year 年份
* @param quarter 季度
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter of(int year, Quarter quarter) {
return new YearQuarter(YEAR.checkValidIntValue(year), Objects.requireNonNull(quarter));
}
/**
* 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例
*
* @param date 日期
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter of(LocalDate date) {
checkNotNull(date);
return new YearQuarter(date.getYear(), Quarter.fromMonth(date.getMonth()));
}
/**
* 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例
*
* @param date 日期
* @return {@link YearQuarter} 实例
*
* @deprecated
* 此方法使用系统默认时区,不建议使用。
* 请使用 {@link #of(Date,ZoneId)}、{@link #of(Date,TimeZone)} 或其它工厂方法
*/
@Deprecated
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter of(Date date) {
checkNotNull(date);
final int yearValue = YEAR.checkValidIntValue(date.getYear() + 1900L);
final int monthValue = date.getMonth() + 1;
return new YearQuarter(yearValue, Quarter.fromMonth(monthValue));
}
/**
* 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例
*
* @param date 日期
* @param zoneId 时区
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter of(Date date, ZoneId zoneId) {
checkNotNull(date);
checkNotNull(zoneId);
LocalDate localDate = date.toInstant().atZone(zoneId).toLocalDate();
return YearQuarter.of(localDate);
}
/**
* 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例
*
* @param date 日期
* @param timeZone 时区
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter of(Date date, TimeZone timeZone) {
checkNotNull(date);
checkNotNull(timeZone);
LocalDate localDate = date.toInstant().atZone(timeZone.toZoneId()).toLocalDate();
return YearQuarter.of(localDate);
}
/**
* 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例
*
* @param date 日期
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter of(Calendar date) {
checkNotNull(date);
final int yearValue = ChronoField.YEAR.checkValidIntValue(date.get(Calendar.YEAR));
final int monthValue = date.get(Calendar.MONTH) + 1;
return new YearQuarter(yearValue, Quarter.fromMonth(monthValue));
}
/**
* 根据指定年月,判断其所在的年份与季度,创建 {@link YearQuarter} 实例
*
* @param yearMonth 年月
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter of(YearMonth yearMonth) {
checkNotNull(yearMonth);
return of(yearMonth.getYear(), Quarter.fromMonth(yearMonth.getMonth()));
}
/**
* 根据现在的日期,判断所在的年份与季度,创建 {@link YearQuarter} 实例
*
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter now() {
return of(LocalDate.now());
}
// #endregion
// #region - Getters
/**
* 年份
*
* @return 年份
*/
public int getYear() {
return this.year;
}
/**
* 季度
*
* @return 季度
*/
public Quarter getQuarter() {
return this.quarter;
}
/**
* 季度值。从 1 开始。
*
* @return 季度值
*/
public int getQuarterValue() {
return this.quarter.getValue();
}
/**
* 该季度第一个月
*
* @return {@link YearMonth} 对象
*/
public YearMonth firstYearMonth() {
return YearMonth.of(this.year, this.quarter.firstMonth());
}
/**
* 该季度第一个月
*
* @return {@link Month} 对象
*/
public Month firstMonth() {
return this.quarter.firstMonth();
}
/**
* 该季度的第一个月
*
* @return 结果。月份值从 1 开始1 表示 1月以此类推。
*/
public int firstMonthValue() {
return this.quarter.firstMonthValue();
}
/**
* 该季度的最后一个月
*
* @return {@link YearMonth} 对象
*/
public YearMonth lastYearMonth() {
return YearMonth.of(this.year, this.quarter.lastMonth());
}
/**
* 该季度的最后一个月
*
* @return {@link Month} 对象
*/
public Month lastMonth() {
return this.quarter.lastMonth();
}
/**
* 该季度的最后一个月
*
* @return 结果。月份值从 1 开始1 表示 1月以此类推。
*/
public int lastMonthValue() {
return this.quarter.lastMonthValue();
}
/**
* 该季度的第一天
*
* @return {@link LocalDate} 对象
*/
public LocalDate firstDate() {
return firstDate;
}
/**
* 该季度的最后一天
*
* @return {@link LocalDate} 对象
*/
public LocalDate lastDate() {
return lastDate;
}
// #endregion
// #region - computes
public YearQuarter plusQuarters(long quartersToAdd) {
if (quartersToAdd == 0L) {
return this;
}
long quarterCount = (this.year - 1) * 4L + (this.quarter.getValue()) + quartersToAdd;
int newYear = YEAR.checkValidIntValue(Math.floorDiv(quarterCount - 1, 4) + 1);
return new YearQuarter(newYear, this.quarter.plus(quartersToAdd));
}
public YearQuarter minusQuarters(long quartersToAdd) {
return plusQuarters(-quartersToAdd);
}
public YearQuarter nextQuarter() {
return plusQuarters(1L);
}
public YearQuarter lastQuarter() {
return minusQuarters(1L);
}
public YearQuarter plusYears(long yearsToAdd) {
if (yearsToAdd == 0L) {
return this;
}
int newYear = YEAR.checkValidIntValue(this.year + yearsToAdd); // safe overflow
return new YearQuarter(newYear, this.quarter);
}
public YearQuarter minusYears(long yearsToAdd) {
return plusYears(-yearsToAdd);
}
public YearQuarter nextYear() {
return plusYears(1L);
}
public YearQuarter lastYear() {
return minusYears(1L);
}
// #endregion
// #region - hashCode & equals
@Override
public int hashCode() {
return Objects.hash(year, quarter);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
YearQuarter other = (YearQuarter) obj;
return year == other.year && quarter == other.quarter;
}
// #endregion
// #region - compare
@SuppressWarnings("null")
@Override
public int compareTo(YearQuarter other) {
int cmp = (this.year - other.year);
if (cmp == 0) {
cmp = this.quarter.compareTo(other.quarter);
}
return cmp;
}
public boolean isBefore(YearQuarter other) {
return this.compareTo(other) < 0;
}
public boolean isAfter(YearQuarter other) {
return this.compareTo(other) > 0;
}
// #endregion
// #region - toString
/**
* 返回 {@link YearQuarter} 的字符串表示形式,如 "2024 Q3"
*
* @return {@link YearQuarter} 的字符串表示形式
*/
@Override
public String toString() {
return this.year + " " + this.quarter.name();
}
// #endregion
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* <h2>时间 API</h2>
*
* <h3>1. 季度 API</h3>
*
* 模仿 JDK 的 {@link java.time.Month} 和 {@link java.time.YearMonth}
* 实现 {@link Quarter}{@link YearQuarter},对季度进行建模。
*
* @author ZhouXY
*/
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.time;
import javax.annotation.ParametersAreNonnullByDefault;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,485 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import java.util.Optional;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.exception.DataNotExistsException;
/**
* 断言工具
*
* <p>
* 本工具类不封装过多判断逻辑,鼓励充分使用项目中的工具类进行逻辑判断。
*
* <pre>
* checkArgument(StringUtils.hasText(str), "The argument cannot be blank.");
* checkState(ArrayUtils.isNotEmpty(result), "The result cannot be empty.");
* checkCondition(!CollectionUtils.isEmpty(roles),
* () -&gt; new InvalidInputException("The roles cannot be empty."));
* checkCondition(RegexTools.matches(email, PatternConsts.EMAIL),
* "must be a well-formed email address");
* </pre>
*
* @author ZhouXY
*/
public class AssertTools {
// ================================
// #region - Argument
// ================================
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition) {
if (!condition) {
throw new IllegalArgumentException();
}
}
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @param errorMessage 异常信息
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition, @Nullable String errorMessage) {
if (!condition) {
throw new IllegalArgumentException(errorMessage);
}
}
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @param errorMessageSupplier 异常信息
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition, Supplier<String> errorMessageSupplier) {
if (!condition) {
throw new IllegalArgumentException(errorMessageSupplier.get());
}
}
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition,
String errorMessageTemplate, Object... errorMessageArgs) {
if (!condition) {
throw new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
// #endregion - Argument
// ================================
// ================================
// #region - ArgumentNotNull
// ================================
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(@Nullable T obj) {
if (obj == null) {
throw new IllegalArgumentException();
}
return obj;
}
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessage 异常信息
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(@Nullable T obj, String errorMessage) {
if (obj == null) {
throw new IllegalArgumentException(errorMessage);
}
return obj;
}
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageSupplier 异常信息
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(@Nullable T obj, Supplier<String> errorMessageSupplier) {
if (obj == null) {
throw new IllegalArgumentException(errorMessageSupplier.get());
}
return obj;
}
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(@Nullable T obj,
String errorMessageTemplate, Object... errorMessageArgs) {
if (obj == null) {
throw new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs));
}
return obj;
}
// ================================
// #endregion - ArgumentNotNull
// ================================
// ================================
// #region - State
// ================================
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition) {
if (!condition) {
throw new IllegalStateException();
}
}
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @param errorMessage 异常信息
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition, @Nullable String errorMessage) {
if (!condition) {
throw new IllegalStateException(errorMessage);
}
}
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @param errorMessageSupplier 异常信息
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition, Supplier<String> errorMessageSupplier) {
if (!condition) {
throw new IllegalStateException(errorMessageSupplier.get());
}
}
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition,
String errorMessageTemplate, Object... errorMessageArgs) {
if (!condition) {
throw new IllegalStateException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
// #endregion - State
// ================================
// ================================
// #region - NotNull
// ================================
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(@Nullable T obj) {
if (obj == null) {
throw new NullPointerException();
}
}
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessage 异常信息
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(@Nullable T obj, String errorMessage) {
if (obj == null) {
throw new NullPointerException(errorMessage);
}
}
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageSupplier 异常信息
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(@Nullable T obj, Supplier<String> errorMessageSupplier) {
if (obj == null) {
throw new NullPointerException(errorMessageSupplier.get());
}
}
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(@Nullable T obj,
String errorMessageTemplate, Object... errorMessageArgs) {
if (obj == null) {
throw new NullPointerException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
// #endregion - NotNull
// ================================
// ================================
// #region - Exists
// ================================
/**
* 检查数据是否存在
*
* @param <T> 入参类型
* @param obj 入参
* @return 如果 {@code obj} 存在,返回 {@code obj} 本身
* @throws DataNotExistsException 当 {@code obj} 不存在时抛出
*/
public static <T> T checkExists(@Nullable T obj)
throws DataNotExistsException {
if (obj == null) {
throw new DataNotExistsException();
}
return obj;
}
/**
* 检查数据是否存在
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessage 异常信息
* @return 如果 {@code obj} 存在,返回 {@code obj} 本身
* @throws DataNotExistsException 当 {@code obj} 不存在时抛出
*/
public static <T> T checkExists(@Nullable T obj, String errorMessage)
throws DataNotExistsException {
if (obj == null) {
throw new DataNotExistsException(errorMessage);
}
return obj;
}
/**
* 检查数据是否存在
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageSupplier 异常信息
* @return 如果 {@code obj} 存在,返回 {@code obj} 本身
* @throws DataNotExistsException 当 {@code obj} 不存在时抛出
*/
public static <T> T checkExists(@Nullable T obj, Supplier<String> errorMessageSupplier)
throws DataNotExistsException {
if (obj == null) {
throw new DataNotExistsException(errorMessageSupplier.get());
}
return obj;
}
/**
* 检查数据是否存在
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @return 如果 {@code obj} 存在,返回 {@code obj} 本身
* @throws DataNotExistsException 当 {@code obj} 不存在时抛出
*/
public static <T> T checkExists(@Nullable T obj,
String errorMessageTemplate, Object... errorMessageArgs)
throws DataNotExistsException {
if (obj == null) {
throw new DataNotExistsException(String.format(errorMessageTemplate, errorMessageArgs));
}
return obj;
}
/**
* 检查数据是否存在
*
* @param <T> 入参类型
* @param optional 入参
* @return 如果 {@code optional} 存在,返回 {@code optional} 包含的值
* @throws DataNotExistsException 当 {@code optional} 的值不存在时抛出
*/
public static <T> T checkExists(Optional<T> optional)
throws DataNotExistsException {
if (!optional.isPresent()) {
throw new DataNotExistsException();
}
return optional.get();
}
/**
* 检查数据是否存在
*
* @param <T> 入参类型
* @param optional 入参
* @param errorMessage 异常信息
* @return 如果 {@code optional} 存在,返回 {@code optional} 包含的值
* @throws DataNotExistsException 当 {@code optional} 的值不存在时抛出
*/
public static <T> T checkExists(Optional<T> optional, String errorMessage)
throws DataNotExistsException {
if (!optional.isPresent()) {
throw new DataNotExistsException(errorMessage);
}
return optional.get();
}
/**
* 检查数据是否存在
*
* @param <T> 入参类型
* @param optional 入参
* @param errorMessageSupplier 异常信息
* @return 如果 {@code optional} 存在,返回 {@code optional} 包含的值
* @throws DataNotExistsException 当 {@code optional} 的值不存在时抛出
*/
public static <T> T checkExists(Optional<T> optional, Supplier<String> errorMessageSupplier)
throws DataNotExistsException {
if (!optional.isPresent()) {
throw new DataNotExistsException(errorMessageSupplier.get());
}
return optional.get();
}
/**
* 检查数据是否存在
*
* @param <T> 入参类型
* @param optional 入参
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @return 如果 {@code optional} 存在,返回 {@code optional} 包含的值
* @throws DataNotExistsException 当 {@code optional} 的值不存在时抛出
*/
public static <T> T checkExists(Optional<T> optional,
String errorMessageTemplate, Object... errorMessageArgs)
throws DataNotExistsException {
if (!optional.isPresent()) {
throw new DataNotExistsException(String.format(errorMessageTemplate, errorMessageArgs));
}
return optional.get();
}
// ================================
// #endregion - Exists
// ================================
// ================================
// #region - Condition
// ================================
/**
* 当条件不满足时抛出异常。
*
* @param <T> 异常类型
* @param condition 条件
* @param e 异常
* @throws T 当条件不满足时抛出异常
*/
public static <T extends Exception> void checkCondition(boolean condition, Supplier<T> e)
throws T {
if (!condition) {
throw e.get();
}
}
// ================================
// #endregion
// ================================
// ================================
// #region - constructor
// ================================
private AssertTools() {
throw new IllegalStateException("Utility class");
}
// ================================
// #endregion
// ================================
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkNotNull;
import java.math.BigDecimal;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
/**
* BigDecimals
*
* <p>
* BigDecimal 工具类
*
* @author ZhouXY
* @since 1.0.0
*/
public class BigDecimals {
/**
* 判断两个 {@code BigDecimal} 的值是否相等
*
* @param a 第一个 {@code BigDecimal}
* @param b 第二个 {@code BigDecimal}
* @return 当两个 {@code BigDecimal} 的值相等时返回 {@code true}
*/
public static boolean equalsValue(@Nullable BigDecimal a, @Nullable BigDecimal b) {
return (a == b) || (a != null && b != null && a.compareTo(b) == 0);
}
/**
* 判断两个 {@code BigDecimal} 的值 - 大于
*
* @param a 第一个 {@code BigDecimal}
* @param b 第二个 {@code BigDecimal}
* @return 当 {@code a} 大于 {@code b} 时返回 {@code true}
*/
public static boolean gt(BigDecimal a, BigDecimal b) {
checkNotNull(a, "Parameter could not be null.");
checkNotNull(b, "Parameter could not be null.");
return (a != b) && (a.compareTo(b) > 0);
}
/**
* 判断两个 {@code BigDecimal} 的值 - 大于等于
*
* @param a 第一个 {@code BigDecimal}
* @param b 第二个 {@code BigDecimal}
* @return 当 {@code a} 大于等于 {@code b} 时返回 {@code true}
*/
public static boolean ge(BigDecimal a, BigDecimal b) {
checkNotNull(a, "Parameter could not be null.");
checkNotNull(b, "Parameter could not be null.");
return (a == b) || (a.compareTo(b) >= 0);
}
/**
* 判断两个 {@code BigDecimal} 的值 - 小于
*
* @param a 第一个 {@code BigDecimal}
* @param b 第二个 {@code BigDecimal}
* @return 当 {@code a} 小于 {@code b} 时返回 {@code true}
*/
public static boolean lt(BigDecimal a, BigDecimal b) {
checkNotNull(a, "Parameter could not be null.");
checkNotNull(b, "Parameter could not be null.");
return (a != b) && (a.compareTo(b) < 0);
}
/**
* 判断两个 {@code BigDecimal} 的值 - 小于等于
*
* @param a 第一个 {@code BigDecimal}
* @param b 第二个 {@code BigDecimal}
* @return 当 {@code a} 小于等于 {@code b} 时返回 {@code true}
*/
public static boolean le(BigDecimal a, BigDecimal b) {
checkNotNull(a, "Parameter could not be null.");
checkNotNull(b, "Parameter could not be null.");
return (a == b) || (a.compareTo(b) <= 0);
}
/**
* 求和
*
* @param numbers {@code BigDecimal} 数组
* @return 求和结果
*/
public static BigDecimal sum(final BigDecimal... numbers) {
if (ArrayTools.isEmpty(numbers)) {
return BigDecimal.ZERO;
}
BigDecimal result = BigDecimals.nullToZero(numbers[0]);
for (int i = 1; i < numbers.length; i++) {
BigDecimal value = numbers[i];
if (value != null) {
result = result.add(value);
}
}
return result;
}
/**
* 将 {@code null} 转换为 {@link BigDecimal#ZERO}
*
* @param val BigDecimal 对象
* @return 如果 {@code val} 为 {@code null},则返回 {@link BigDecimal#ZERO},否则返回 {@code val}
*/
@Nonnull
public static BigDecimal nullToZero(@Nullable final BigDecimal val) {
return val != null ? val : BigDecimal.ZERO;
}
/**
* 获取字符串所表示的数值转换为 {@code BigDecimal}
*
* @param val 表示数值的字符串
* @return {@code BigDecimal} 对象
*/
@StaticFactoryMethod(BigDecimal.class)
public static BigDecimal of(@Nullable final String val) {
return (StringTools.isNotBlank(val)) ? new BigDecimal(val) : BigDecimal.ZERO;
}
private BigDecimals() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,778 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static java.time.temporal.ChronoField.*;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
import xyz.zhouxy.plusone.commons.time.Quarter;
import xyz.zhouxy.plusone.commons.time.YearQuarter;
/**
* 日期时间工具类
*
* @author ZhouXY
*/
public class DateTimeTools {
// ================================
// #region - toString
// ================================
public static String toYearString(int year) {
return Integer.toString(YEAR.checkValidIntValue(year));
}
public static String toYearString(Year year) {
return year.toString();
}
public static String toMonthStringM(int monthValue) {
return Integer.toString(MONTH_OF_YEAR.checkValidIntValue(monthValue));
}
public static String toMonthStringMM(int monthValue) {
return String.format("%02d", MONTH_OF_YEAR.checkValidIntValue(monthValue));
}
public static String toMonthStringM(Month month) {
return Integer.toString(month.getValue());
}
public static String toMonthStringMM(Month month) {
return String.format("%02d", month.getValue());
}
// ================================
// #endregion
// ================================
// ================================
// #region - toDate
// ================================
/**
* 将时间戳转换为 {@link Date} 对象
*
* @param timeMillis 时间戳
* @return {@link Date} 对象
*/
public static Date toDate(long timeMillis) {
return Date.from(Instant.ofEpochMilli(timeMillis));
}
/**
* 将 {@link Calendar} 对象转换为 {@link Date} 对象
*
* @param calendar {@link Calendar} 对象
* @return {@link Date} 对象
*/
public static Date toDate(Calendar calendar) {
return calendar.getTime();
}
/**
* 将 {@link Instant} 对象转换为 {@link Date} 对象
*
* @param instant {@link Instant} 对象
* @return {@link Date} 对象
*/
public static Date toDate(Instant instant) {
return Date.from(instant);
}
/**
* 将 {@link ZonedDateTime} 对象转换为 {@link Date} 对象
*
* @param zonedDateTime {@link ZonedDateTime} 对象
* @return {@link Date} 对象
*/
public static Date toDate(ZonedDateTime zonedDateTime) {
return Date.from(zonedDateTime.toInstant());
}
/**
* 使用指定时区,将 {@link LocalDateTime} 对象转换为 {@link Date} 对象
*
* @param localDateTime {@link LocalDateTime} 对象
* @param zone 时区
* @return {@link Date} 对象
*/
public static Date toDate(LocalDateTime localDateTime, ZoneId zone) {
return Date.from(ZonedDateTime.of(localDateTime, zone).toInstant());
}
/**
* 使用指定时区,将 {@link LocalDate} 和 {@link LocalTime} 对象转换为 {@link Date} 对象
*
* @param localDate {@link LocalDate} 对象
* @param localTime {@link LocalTime} 对象
* @param zone 时区
* @return {@link Date} 对象
*/
public static Date toDate(LocalDate localDate, LocalTime localTime, ZoneId zone) {
return Date.from(ZonedDateTime.of(localDate, localTime, zone).toInstant());
}
// ================================
// #endregion
// ================================
// ================================
// #region - toInstant
// ================================
/**
* 将时间戳转换为 {@link Instant} 对象
*
* @param timeMillis 时间戳
* @return {@link Instant} 对象
*/
public static Instant toInstant(long timeMillis) {
return Instant.ofEpochMilli(timeMillis);
}
/**
* 将 {@link Date} 对象转换为 {@link Instant} 对象
*
* @param date {@link Date} 对象
* @return {@link Instant} 对象
*/
public static Instant toInstant(Date date) {
return date.toInstant();
}
/**
* 将 {@link Calendar} 对象转换为 {@link Instant} 对象
*
* @param calendar {@link Calendar} 对象
* @return {@link Instant} 对象
*/
public static Instant toInstant(Calendar calendar) {
return calendar.toInstant();
}
/**
* 将 {@link ZonedDateTime} 对象转换为 {@link Instant} 对象
*
* @param zonedDateTime {@link ZonedDateTime} 对象
* @return {@link Instant} 对象
*/
public static Instant toInstant(ZonedDateTime zonedDateTime) { // NOSONAR
return zonedDateTime.toInstant();
}
/**
* 使用指定时区,将 {@link LocalDateTime} 对象转换为 {@link Instant} 对象
*
* @param localDateTime {@link LocalDateTime} 对象
* @param zone 时区
* @return {@link Instant} 对象
*/
public static Instant toInstant(LocalDateTime localDateTime, ZoneId zone) {
return ZonedDateTime.of(localDateTime, zone).toInstant();
}
// ================================
// #endregion
// ================================
// ================================
// #region - toZonedDateTime
// ================================
/**
* 获取时间戳在指定时区的地区时间。
* <p>
* 传入不同 {@link ZoneId},获取到的 {@link ZonedDateTime} 对象实际上还是同一时间戳,
* 只是不同时区的表示。
*
* @param timeMillis 时间戳
* @param zone 时区
* @return 带时区信息的地区时间
*/
public static ZonedDateTime toZonedDateTime(long timeMillis, ZoneId zone) {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timeMillis), zone);
}
/**
* 获取 {@link Date} 所表示的时间戳,在指定时区的地区时间。
* <p>
* 传入不同 {@link ZoneId},获取到的 {@link ZonedDateTime} 对象实际上还是同一时间戳,
* 只是不同时区的表示。
*
* @param dateTime {@link Date} 对象
* @param zone 时区
* @return 带时区信息的地区时间
*/
public static ZonedDateTime toZonedDateTime(Date dateTime, ZoneId zone) {
return ZonedDateTime.ofInstant(dateTime.toInstant(), zone);
}
/**
* 获取 {@link Date} 所表示的时间戳,在指定时区的地区时间。
* <p>
* 传入不同 {@link ZoneId},获取到的 {@link ZonedDateTime} 对象实际上表示的还是还是同一时间戳的时间,
* 只是不同时区的表示。
*
* @param dateTime {@link Date} 对象
* @param timeZone 时区
* @return 带时区信息的地区时间
*/
public static ZonedDateTime toZonedDateTime(Date dateTime, TimeZone timeZone) {
return ZonedDateTime.ofInstant(dateTime.toInstant(), timeZone.toZoneId());
}
/**
* 使用 {@code calendar} 对象的时区信息,将 {@link Calendar} 对象转换为 {@link ZonedDateTime}
* 对象。
*
* @param calendar{@link Calendar} 对象
* @return {@link ZonedDateTime} 对象
*/
public static ZonedDateTime toZonedDateTime(Calendar calendar) {
return calendar.toInstant().atZone(calendar.getTimeZone().toZoneId());
}
/**
* 使用指定的时区,将 {@link Calendar} 对象转换为 {@link ZonedDateTime} 对象。
*
* @param calendar {@link Calendar} 对象
* @param zone 时区
* @return {@link ZonedDateTime} 对象
*/
public static ZonedDateTime toZonedDateTime(Calendar calendar, ZoneId zone) {
return calendar.toInstant().atZone(zone);
}
/**
* 使用指定的时区,将 {@link Calendar} 对象转换为 {@link ZonedDateTime} 对象。
*
* @param calendar {@link Calendar} 对象
* @param zone 时区
* @return {@link ZonedDateTime} 对象
*/
public static ZonedDateTime toZonedDateTime(Calendar calendar, TimeZone zone) {
return calendar.toInstant().atZone(zone.toZoneId());
}
/**
* 创建带时区的地区时间
*
* @param localDateTime 地区时间
* @param zone 时区
* @return 带时区的地区时间
*/
public static ZonedDateTime toZonedDateTime(LocalDateTime localDateTime, ZoneId zone) {
return ZonedDateTime.of(localDateTime, zone);
}
// ================================
// #endregion
// ================================
// ================================
// #region - toLocalDateTime
// ================================
/**
* 获取时间戳在指定时区的地区时间。
*
* @param timeMillis 时间戳
* @param zone 时区
* @return 地区时间
*/
public static LocalDateTime toLocalDateTime(long timeMillis, ZoneId zone) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timeMillis), zone);
}
/**
* 获取 {@link Date} 所表示的时间戳,在指定时区的地区时间。
*
* @param dateTime {@link Date} 对象
* @param zone 时区
* @return 地区时间
*/
public static LocalDateTime toLocalDateTime(Date dateTime, ZoneId zone) {
return LocalDateTime.ofInstant(dateTime.toInstant(), zone);
}
/**
* 获取 {@link Date} 所表示的时间戳,在指定时区的地区时间。
*
* @param dateTime {@link Date} 对象
* @param timeZone 时区
* @return 地区时间
*/
public static LocalDateTime toLocalDateTime(Date dateTime, TimeZone timeZone) {
return LocalDateTime.ofInstant(dateTime.toInstant(), timeZone.toZoneId());
}
/**
* 获取 {@link Calendar} 所表示的时间戳,在指定时区的地区时间。
*
* @param calendar {@link Calendar} 对象
* @param zone 时区
* @return 地区时间
*/
public static LocalDateTime toLocalDateTime(Calendar calendar, ZoneId zone) {
return LocalDateTime.ofInstant(calendar.toInstant(), zone);
}
/**
* 获取 {@link Calendar} 所表示的时间戳,在指定时区的地区时间。
*
* @param calendar {@link Calendar} 对象
* @param zone 时区
* @return 地区时间
*/
public static LocalDateTime toLocalDateTime(Calendar calendar, TimeZone zone) {
return LocalDateTime.ofInstant(calendar.toInstant(), zone.toZoneId());
}
/**
* 获取 {@link ZonedDateTime} 所表示的时间戳,在指定时区的地区时间。
*
* @param zonedDateTime {@link ZonedDateTime} 对象
* @param zone 时区
* @return 地区时间
*/
public static LocalDateTime toLocalDateTime(ZonedDateTime zonedDateTime, ZoneId zone) {
return LocalDateTime.ofInstant(zonedDateTime.toInstant(), zone);
}
// ================================
// #endregion
// ================================
// ================================
// #region - YearQuarter & Quarter
// ================================
/**
* 获取指定日期所在季度
*
* @param date 日期
* @return 日期所在的季度
*
* @deprecated
* 此方法使用系统默认时区,不建议使用。
* 请使用 {@link #getQuarter(Date,ZoneId)}、{@link #getQuarter(Date,TimeZone)} 或其它工厂方法
*/
@Deprecated
public static YearQuarter getQuarter(Date date) {
return YearQuarter.of(date);
}
/**
* 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例
*
* @param date 日期
* @param timeZone 时区
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter getQuarter(Date date, ZoneId timeZone) {
return YearQuarter.of(date, timeZone);
}
/**
* 根据指定日期,判断日期所在的年份与季度,创建 {@link YearQuarter} 实例
*
* @param date 日期
* @param timeZone 时区
* @return {@link YearQuarter} 实例
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter getQuarter(Date date, TimeZone timeZone) {
return YearQuarter.of(date, timeZone);
}
/**
* 获取指定日期所在季度
*
* @param date 日期
* @return 日期所在的季度
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter getQuarter(Calendar date) {
return YearQuarter.of(date);
}
/**
* 获取指定月份所在季度
*
* @param month 月份
* @return 季度
*/
@StaticFactoryMethod(Quarter.class)
public static Quarter getQuarter(Month month) {
return Quarter.fromMonth(month);
}
/**
* 获取指定年月所在季度
*
* @param year 年
* @param month 月
* @return 季度
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter getQuarter(int year, Month month) {
return YearQuarter.of(YearMonth.of(year, month));
}
/**
* 获取指定年月所在季度
*
* @param yearMonth 年月
* @return 季度
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter getQuarter(YearMonth yearMonth) {
return YearQuarter.of(yearMonth);
}
/**
* 获取指定日期所在季度
*
* @param date 日期
* @return 日期所在的季度
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter getQuarter(LocalDate date) {
return YearQuarter.of(date);
}
// ================================
// #endregion
// ================================
// ================================
// #region - start & end
// ================================
/**
* 获取指定年份的开始日期
*
* @param year 年份
* @return 指定年份的开始日期
*/
public static LocalDate startDateOfYear(int year) {
return LocalDate.ofYearDay(year, 1);
}
/**
* 获取指定年份的结束日期
*
* @param year 年份
* @return 指定年份的结束日期
*/
public static LocalDate endDateOfYear(int year) {
return LocalDate.of(year, 12, 31);
}
/**
* 获取指定日期的第二天的开始时间
*
* @param date 日期
* @return 指定日期的第二天的开始时间
*/
public static LocalDateTime startOfNextDate(LocalDate date) {
return date.plusDays(1L).atStartOfDay();
}
/**
* 获取指定日期的第二天的开始时间
*
* @param date 日期
* @param zone 时区
* @return 指定日期的第二天的开始时间
*/
public static ZonedDateTime startOfNextDate(LocalDate date, ZoneId zone) {
return date.plusDays(1L).atStartOfDay(zone);
}
// ================================
// #endregion - start & end
// ================================
// ================================
// #region - isFuture
// ================================
/**
* 判断指定日期时间是否在将来
*
* @param date 日期时间
* @return 指定日期时间是否在将来
*/
public static boolean isFuture(Date date) {
return date.after(new Date());
}
/**
* 判断指定日期时间是否在将来
*
* @param calendar 日期时间
* @return 指定日期时间是否在将来
*/
public static boolean isFuture(Calendar calendar) {
return calendar.after(Calendar.getInstance());
}
/**
* 判断指定时刻是否在将来
*
* @param instant 时刻
* @return 指定时刻是否在将来
*/
public static boolean isFuture(Instant instant) {
return instant.isAfter(Instant.now());
}
/**
* 判断指定时间戳是否在将来
*
* @param timeMillis 时间戳
* @return 指定时间戳是否在将来
*/
public static boolean isFuture(long timeMillis) {
return timeMillis > System.currentTimeMillis();
}
/**
* 判断指定日期是否在将来
*
* @param date 日期
* @return 指定日期是否在将来
*/
public static boolean isFuture(LocalDate date) {
return date.isAfter(LocalDate.now());
}
/**
* 判断指定日期时间是否在将来
*
* @param dateTime 日期时间
* @return 指定日期时间是否在将来
*/
public static boolean isFuture(LocalDateTime dateTime) {
return dateTime.isAfter(LocalDateTime.now());
}
/**
* 判断指定日期时间是否在将来
*
* @param dateTime 日期时间
* @return 指定日期时间是否在将来
*/
public static boolean isFuture(ZonedDateTime dateTime) {
return dateTime.isAfter(ZonedDateTime.now());
}
// ================================
// #endregion - isFuture
// ================================
// ================================
// #region - isPast
// ================================
/**
* 判断指定日期时间是否在过去
*
* @param date 日期时间
* @return 指定日期时间是否在过去
*/
public static boolean isPast(Date date) {
return date.before(new Date());
}
/**
* 判断指定日期时间是否在过去
*
* @param calendar 日期时间
* @return 指定日期时间是否在过去
*/
public static boolean isPast(Calendar calendar) {
return calendar.before(Calendar.getInstance());
}
/**
* 判断指定时刻是否在过去
*
* @param instant 时刻
* @return 指定时刻是否在过去
*/
public static boolean isPast(Instant instant) {
return instant.isBefore(Instant.now());
}
/**
* 判断指定时间戳是否在过去
*
* @param timeMillis 时间戳
* @return 指定时间戳是否在过去
*/
public static boolean isPast(long timeMillis) {
return timeMillis < System.currentTimeMillis();
}
/**
* 判断指定日期是否在过去
*
* @param date 日期
* @return 指定日期是否在过去
*/
public static boolean isPast(LocalDate date) {
return date.isBefore(LocalDate.now());
}
/**
* 判断指定日期时间是否在过去
*
* @param dateTime 日期时间
* @return 指定日期时间是否在过去
*/
public static boolean isPast(LocalDateTime dateTime) {
return dateTime.isBefore(LocalDateTime.now());
}
/**
* 判断指定日期时间是否在过去
*
* @param dateTime 日期时间
* @return 指定日期时间是否在过去
*/
public static boolean isPast(ZonedDateTime dateTime) {
return dateTime.isBefore(ZonedDateTime.now());
}
// ================================
// #endregion - isPast
// ================================
// ================================
// #region - range
// ================================
/**
* 获取指定日期的时间范围
*
* @param date 日期
* @return 指定日期的时间范围
*/
public static Range<LocalDateTime> toDateTimeRange(LocalDate date) {
return Range.closedOpen(date.atStartOfDay(), startOfNextDate(date));
}
/**
* 获取指定日期的时间范围
*
* @param date 日期
* @param zone 时区
* @return 指定日期的时间范围
*/
public static Range<ZonedDateTime> toDateTimeRange(LocalDate date, ZoneId zone) {
return Range.closedOpen(date.atStartOfDay(zone), startOfNextDate(date, zone));
}
/**
* 将指定日期范围转为日期时间范围
*
* @param dateRange 日期范围
* @return 对应的日期时间范围
*/
public static Range<LocalDateTime> toDateTimeRange(Range<LocalDate> dateRange) {
BoundType lowerBoundType = dateRange.lowerBoundType();
LocalDateTime lowerEndpoint = lowerBoundType == BoundType.CLOSED
? dateRange.lowerEndpoint().atStartOfDay()
: dateRange.lowerEndpoint().plusDays(1).atStartOfDay();
BoundType upperBoundType = dateRange.upperBoundType();
LocalDateTime upperEndpoint = upperBoundType == BoundType.CLOSED
? dateRange.upperEndpoint().plusDays(1).atStartOfDay()
: dateRange.upperEndpoint().atStartOfDay();
return Range.closedOpen(lowerEndpoint, upperEndpoint);
}
/**
* 将指定日期范围转为日期时间范围
*
* @param dateRange 日期范围
* @param zone 时区
* @return 对应的日期时间范围
*/
public static Range<ZonedDateTime> toDateTimeRange(Range<LocalDate> dateRange, ZoneId zone) {
BoundType lowerBoundType = dateRange.lowerBoundType();
ZonedDateTime lowerEndpoint = lowerBoundType == BoundType.CLOSED
? dateRange.lowerEndpoint().atStartOfDay(zone)
: dateRange.lowerEndpoint().plusDays(1).atStartOfDay(zone);
BoundType upperBoundType = dateRange.upperBoundType();
ZonedDateTime upperEndpoint = upperBoundType == BoundType.CLOSED
? dateRange.upperEndpoint().plusDays(1).atStartOfDay(zone)
: dateRange.upperEndpoint().atStartOfDay(zone);
return Range.closedOpen(lowerEndpoint, upperEndpoint);
}
// ================================
// #endregion - range
// ================================
// ================================
// #region - others
// ================================
/**
* 判断指定年份是否为闰年
*
* @param year 年份
* @return 指定年份是否为闰年
*/
public static boolean isLeapYear(int year) {
return Year.isLeap(year);
}
// ================================
// #endregion - others
// ================================
/**
* 私有构造方法
*/
private DateTimeTools() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,213 @@
/*
* Copyright 2022-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkCondition;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkNotNull;
import java.util.function.Supplier;
import javax.annotation.Nullable;
/**
* 枚举工具类
*
* @author ZhouXY
*/
public final class EnumTools {
private EnumTools() {
throw new IllegalStateException("Utility class");
}
/**
* 通过 ordinal 获取枚举实例
*
* @param <E> 枚举的类型
* @param enumType 枚举的类型信息
* @param ordinal 序号
* @return 枚举对象
* @deprecated 不推荐使用枚举的 ordinal。
*/
@Deprecated
private static <E extends Enum<?>> E valueOfInternal(Class<E> enumType, int ordinal) { // NOSONAR 该方法弃用,但不删掉
E[] values = enumType.getEnumConstants();
checkCondition((ordinal >= 0 && ordinal < values.length),
() -> new EnumConstantNotPresentException(enumType, Integer.toString(ordinal)));
return values[ordinal];
}
/**
* 通过 ordinal 获取枚举实例
*
* @param <E> 枚举的类型
* @param enumType 枚举的类型信息
* @param ordinal 序号
* @return 枚举对象
* @deprecated 不推荐使用枚举的 ordinal。
*/
@Deprecated
public static <E extends Enum<?>> E valueOf(Class<E> enumType, int ordinal) { // NOSONAR 该方法弃用,但不删掉
checkNotNull(enumType, "Enum type must not be null.");
return valueOfInternal(enumType, ordinal);
}
/**
* 通过 ordinal 获取枚举实例
*
* @param <E> 枚举的类型
* @param enumType 枚举的类型信息
* @param ordinal 序号
* @param defaultValue 默认值
* @return 枚举对象
* @deprecated 不推荐使用枚举的 ordinal。
*/
@Deprecated
public static <E extends Enum<?>> E valueOf(Class<E> enumType, // NOSONAR 该方法弃用,但不删掉
@Nullable Integer ordinal, @Nullable E defaultValue) {
checkNotNull(enumType);
return null == ordinal ? defaultValue : valueOfInternal(enumType, ordinal);
}
/**
* 通过 ordinal 获取枚举实例
*
* @param <E> 枚举的类型
* @param enumType 枚举的类型信息
* @param ordinal 序号
* @param defaultValue 默认值
* @return 枚举对象
* @deprecated 不推荐使用枚举的 ordinal。
*/
@Deprecated
public static <E extends Enum<?>> E getValueOrDefault( // NOSONAR 该方法弃用,但不删掉
Class<E> enumType,
@Nullable Integer ordinal,
Supplier<E> defaultValue) {
checkNotNull(enumType);
checkNotNull(defaultValue);
return null == ordinal ? defaultValue.get() : valueOfInternal(enumType, ordinal);
}
/**
* 通过 ordinal 获取枚举实例
*
* @param <E> 枚举的类型
* @param enumType 枚举的类型信息
* @param ordinal 序号
* @return 枚举对象
* @deprecated 不推荐使用枚举的 ordinal。
*/
@Deprecated
public static <E extends Enum<?>> E getValueOrDefault(Class<E> enumType, @Nullable Integer ordinal) { // NOSONAR 该方法弃用,但不删掉
return getValueOrDefault(enumType, ordinal, () -> {
checkNotNull(enumType, "Enum type must not be null.");
E[] values = enumType.getEnumConstants();
return values[0];
});
}
/**
* 通过 ordinal 获取枚举实例
*
* @param <E> 枚举的类型
* @param enumType 枚举的类型信息
* @param ordinal 序号
* @return 枚举对象
* @deprecated 不推荐使用枚举的 ordinal。
*/
@Deprecated
public static <E extends Enum<?>> E getValueNullable(Class<E> enumType, @Nullable Integer ordinal) { // NOSONAR 该方法弃用,但不删掉
return valueOf(enumType, ordinal, null);
}
public static <E extends Enum<?>> Integer checkOrdinal(Class<E> enumType, Integer ordinal) {
checkNotNull(enumType, "Enum type must not be null.");
checkNotNull(ordinal, "Ordinal must not be null.");
E[] values = enumType.getEnumConstants();
checkCondition(ordinal >= 0 && ordinal < values.length,
() -> new EnumConstantNotPresentException(enumType, Integer.toString(ordinal)));
return ordinal;
}
/**
* 校验枚举的 ordinal。
*
* @param <E> 枚举类型
* @param enumType 枚举类型
* @param ordinal The ordinal
* @return The ordinal
*/
@Nullable
public static <E extends Enum<?>> Integer checkOrdinalNullable(Class<E> enumType, @Nullable Integer ordinal) {
return checkOrdinalOrDefault(enumType, ordinal, null);
}
/**
* 校验枚举的 ordinal如果 ordinal 为 {@code null},则返回 {@code 0}。
*
* @param <E> 枚举类型
* @param enumType 枚举类型
* @param ordinal The ordinal
* @return The ordinal
*/
@Nullable
public static <E extends Enum<?>> Integer checkOrdinalOrDefault(Class<E> enumType, @Nullable Integer ordinal) {
return checkOrdinalOrDefault(enumType, ordinal, 0);
}
/**
* 校验枚举的 ordinal如果 ordinal 为 {@code null},则返回 {@code defaultValue}。
*
* @param <E> 枚举类型
* @param enumType 枚举类型
* @param ordinal The ordinal
* @param defaultValue 默认值
* @return The ordinal
*/
@Nullable
public static <E extends Enum<?>> Integer checkOrdinalOrDefault(
Class<E> enumType,
@Nullable Integer ordinal,
@Nullable Integer defaultValue) {
checkNotNull(enumType);
return checkOrdinalOrGetInternal(enumType, ordinal, () -> checkOrdinalOrDefaultInternal(enumType, defaultValue, null));
}
/**
* 仅对 {@code ordinal} 进行判断,不对 {@code defaultValue} 进行判断
*/
@Nullable
private static <E extends Enum<?>> Integer checkOrdinalOrDefaultInternal(
Class<E> enumType,
@Nullable Integer ordinal,
@Nullable Integer defaultValue) {
return ordinal != null
? checkOrdinal(enumType, ordinal)
: defaultValue;
}
@Nullable
private static <E extends Enum<?>> Integer checkOrdinalOrGetInternal(
Class<E> enumType,
@Nullable Integer ordinal,
Supplier<Integer> defaultValueSupplier) {
return ordinal != null
? checkOrdinal(enumType, ordinal)
: defaultValueSupplier.get();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2022-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,39 +16,61 @@
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Preconditions;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
/**
* 枚举类
*
* <p>
* 参考 <a href="https://lostechies.com/jimmybogard/2008/08/12/enumeration-classes/">Enumeration classes</a>
*
* @author ZhouXY
* @deprecated 设计 Enumeration 的灵感来自于 .net 社区因为 C# 的枚举不带行为
* Java 的枚举可以带行为故大多数情况下不需要这种设计
*/
public abstract class Enumeration<T extends Enumeration<T>> implements Comparable<T> {
@Deprecated
public abstract class Enumeration<T extends Enumeration<T>> // NOSONAR 暂不移除
implements Comparable<T> {
protected final int id;
protected final String name;
protected Enumeration(final int id, final String name) {
Preconditions.checkArgument(StringUtils.isNotBlank(name), "Name of enumeration must has text.");
checkArgument(StringTools.isNotBlank(name), "Name of enumeration must has text.");
this.id = id;
this.name = name;
}
/**
* 枚举整数码值
*
* @return 整数码值
*/
public final int getId() {
return id;
}
/**
* 枚举名称
*
* @return 枚举名称
*/
public final String getName() {
return name;
}
@SuppressWarnings("null")
@Override
public final int compareTo(final T o) {
return Integer.compare(this.id, o.id);
@@ -60,7 +82,7 @@ public abstract class Enumeration<T extends Enumeration<T>> implements Comparabl
}
@Override
public final boolean equals(final Object obj) {
public final boolean equals(@Nullable final Object obj) {
if (this == obj)
return true;
if (obj == null)
@@ -76,6 +98,9 @@ public abstract class Enumeration<T extends Enumeration<T>> implements Comparabl
return getClass().getSimpleName() + '(' + id + ":" + name + ')';
}
/**
* 枚举值集合
*/
protected static final class ValueSet<T extends Enumeration<T>> {
private final Map<Integer, T> valueMap;
@@ -83,21 +108,36 @@ public abstract class Enumeration<T extends Enumeration<T>> implements Comparabl
this.valueMap = valueMap;
}
@SafeVarargs
/**
* 创建枚举值集合
*
* @param <T> 枚举类型
* @param values 枚举值
* @return 枚举值集合
*/
@StaticFactoryMethod(ValueSet.class)
public static <T extends Enumeration<T>> ValueSet<T> of(T... values) {
Map<Integer, T> temp = new HashMap<>();
for (T value : values) {
temp.put(value.getId(), value);
}
public static <T extends Enumeration<T>> ValueSet<T> of(T[] values) {
Map<Integer, T> temp = Arrays.stream(values)
.collect(Collectors.toMap(Enumeration::getId, Function.identity()));
return new ValueSet<>(Collections.unmodifiableMap(temp));
}
/**
* 根据整数码值获取枚举对象
*
* @param id 整数码
* @return 枚举对象
*/
public T get(int id) {
Preconditions.checkArgument(this.valueMap.containsKey(id), "%s 对应的值不存在", id);
checkArgument(this.valueMap.containsKey(id), "[%s] 对应的值不存在", id);
return this.valueMap.get(id);
}
/**
* 获取所有枚举对象
*
* @return 所有枚举对象
*/
public Collection<T> getValues() {
return this.valueMap.values();
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* ID 生成器
*
* <p>
* 生成 UUID 和 修改版雪花IDSeata 版本)
*
* @see UUID
* @see IdWorker
* @author ZhouXY
*/
public class IdGenerator {
// ===== UUID =====
/**
* 生成 UUID
*
* @return UUID
*/
public static UUID newUuid() {
return UUID.randomUUID();
}
/**
* 生成 UUID 字符串
*
* @return UUID 字符串
*/
public static String uuidString() {
return UUID.randomUUID().toString();
}
/**
* 生成 UUID 字符串(无分隔符)
*
* @return UUID 字符串
*/
public static String simpleUuidString() {
return toSimpleString(UUID.randomUUID());
}
/**
* 生成 UUID 字符串(无分隔符)
*
* @param uuid UUID
* @return UUID 字符串
*/
public static String toSimpleString(UUID uuid) {
checkArgumentNotNull(uuid);
return (uuidDigits(uuid.getMostSignificantBits() >> 32, 8) +
uuidDigits(uuid.getMostSignificantBits() >> 16, 4) +
uuidDigits(uuid.getMostSignificantBits(), 4) +
uuidDigits(uuid.getLeastSignificantBits() >> 48, 4) +
uuidDigits(uuid.getLeastSignificantBits(), 12));
}
/** Returns val represented by the specified number of hex digits. */
private static String uuidDigits(long val, int digits) {
long hi = 1L << (digits * 4);
return Long.toHexString(hi | (val & (hi - 1))).substring(1);
}
// ===== SnowflakeId =====
private static final Map<Long, IdWorker> snowflakePool = new ConcurrentHashMap<>();
/**
* 生成雪花ID
*
* @param workerId 工作机器ID
* @return 雪花ID
*/
public static long nextSnowflakeId(long workerId) {
IdWorker generator = getSnowflakeIdGenerator(workerId);
return generator.nextId();
}
/**
* 获取雪花ID生成器
*
* @param workerId 工作机器ID
* @return {@code IdWorker} 对象。来自 Seata 的修改版雪花ID生成器。
*/
public static IdWorker getSnowflakeIdGenerator(long workerId) {
return snowflakePool.computeIfAbsent(workerId, wid -> new IdWorker(workerId));
}
private IdGenerator() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,211 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.exception.system.NoAvailableMacFoundException;
/**
* 修改版雪花 ID 生成器
*
* <p>
* 来自 <a href="https://seata.apache.org">Seata</a> 的 {@code org.apache.seata.common.util.IdWorker}
*
* <p>
* 大体思路为:
* <ol>
* <li>每个机器线程安全地生成序列前面加上机器的id这样就不会与其它机器的id相冲突。</li>
* <li>时间戳作为序列的“预留位”,它更像是应用启动时最开始的序列的一部分,在一个时间戳里生成 4096 个 id 之后,直接生成下一个时间戳的 id。</li>
* </ol>
*
* <p>
* 详情见以下介绍:
* <ul>
* <li><a href="https://seata.apache.org/zh-cn/blog/seata-analysis-UUID-generator/">Seata基于改良版雪花算法的分布式UUID生成器分析</a></li>
* <li><a href="https://seata.apache.org/zh-cn/blog/seata-snowflake-explain">关于新版雪花算法的答疑</a></li>
* <li><a href="https://juejin.cn/post/7264387737276203065">在开源项目中看到一个改良版的雪花算法,现在它是你的了。</a></li>
* <li><a href="https://juejin.cn/post/7265516484029743138">关于若干读者,阅读“改良版雪花算法”后提出的几个共性问题的回复。</a></li>
* </ul>
*/
public class IdWorker {
/**
* Start time cut (2020-05-03)
*/
private static final long TWEPOCH = 1588435200000L;
/**
* The number of bits occupied by workerId
*/
private static final int WORKER_ID_BITS = 10;
/**
* The number of bits occupied by timestamp
*/
private static final int TIMESTAMP_BITS = 41;
/**
* The number of bits occupied by sequence
*/
private static final int SEQUENCE_BITS = 12;
/**
* Maximum supported machine id, the result is 1023
*/
private static final int MAX_WORKER_ID = ~(-1 << WORKER_ID_BITS);
/**
* business meaning: machine ID (0 ~ 1023)
* actual layout in memory:
* highest 1 bit: 0
* middle 10 bit: workerId
* lowest 53 bit: all 0
*/
private long workerId;
/**
* timestamp and sequence mix in one Long
* highest 11 bit: not used
* middle 41 bit: timestamp
* lowest 12 bit: sequence
*/
private AtomicLong timestampAndSequence;
/**
* mask that help to extract timestamp and sequence from a long
*/
private static final long TIMESTAMP_AND_SEQUENCE_MASK = ~(-1L << (TIMESTAMP_BITS + SEQUENCE_BITS));
/**
* instantiate an IdWorker using given workerId
* @param workerId if null, then will auto assign one
*/
public IdWorker(Long workerId) {
initTimestampAndSequence();
initWorkerId(workerId);
}
/**
* init first timestamp and sequence immediately
*/
private void initTimestampAndSequence() {
long timestamp = getNewestTimestamp();
long timestampWithSequence = timestamp << SEQUENCE_BITS;
this.timestampAndSequence = new AtomicLong(timestampWithSequence);
}
/**
* init workerId
* @param workerId if null, then auto generate one
*/
private void initWorkerId(@Nullable Long workerId) {
if (workerId == null) {
workerId = generateWorkerId();
}
if (workerId > MAX_WORKER_ID || workerId < 0) {
String message = String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID);
throw new IllegalArgumentException(message);
}
this.workerId = workerId << (TIMESTAMP_BITS + SEQUENCE_BITS);
}
/**
* get next UUID(base on snowflake algorithm), which look like:
* highest 1 bit: always 0
* next 10 bit: workerId
* next 41 bit: timestamp
* lowest 12 bit: sequence
* @return UUID
*/
public long nextId() {
waitIfNecessary();
long next = timestampAndSequence.incrementAndGet();
long timestampWithSequence = next & TIMESTAMP_AND_SEQUENCE_MASK;
return workerId | timestampWithSequence;
}
/**
* block current thread if the QPS of acquiring UUID is too high
* that current sequence space is exhausted
*/
private void waitIfNecessary() {
long currentWithSequence = timestampAndSequence.get();
long current = currentWithSequence >>> SEQUENCE_BITS;
long newest = getNewestTimestamp();
if (current >= newest) {
try {
Thread.sleep(5);
} catch (InterruptedException ignore) { // NOSONAR don't care
}
}
}
/**
* get newest timestamp relative to twepoch
*/
private long getNewestTimestamp() {
return System.currentTimeMillis() - TWEPOCH;
}
/**
* auto generate workerId, try using mac first, if failed, then randomly generate one
* @return workerId
*/
private static long generateWorkerId() {
try {
return generateWorkerIdBaseOnMac();
} catch (Exception e) {
return generateRandomWorkerId();
}
}
/**
* use lowest 10 bit of available MAC as workerId
* @return workerId
* @throws SocketException if an I/O error occurs.
* @throws NoAvailableMacFoundException when there is no available mac found
*/
private static long generateWorkerIdBaseOnMac() throws SocketException, NoAvailableMacFoundException {
Enumeration<NetworkInterface> all = NetworkInterface.getNetworkInterfaces();
while (all.hasMoreElements()) {
NetworkInterface networkInterface = all.nextElement();
boolean isLoopback = networkInterface.isLoopback();
boolean isVirtual = networkInterface.isVirtual();
byte[] mac = networkInterface.getHardwareAddress();
if (isLoopback || isVirtual || mac == null) {
continue;
}
return ((mac[4] & 0B11) << 8) | (mac[5] & 0xFF);
}
throw new NoAvailableMacFoundException();
}
/**
* randomly generate one as workerId
* @return workerId
*/
private static long generateRandomWorkerId() {
return ThreadLocalRandom.current().nextInt(MAX_WORKER_ID + 1);
}
}

View File

@@ -0,0 +1,299 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
/**
* Joda-Time 工具类
*
* @author ZhouXY
*/
public class JodaTimeTools {
// ================================
// #region - toJodaInstant
// ================================
/**
* 将 {@link java.time.Instant} 转换为 {@link org.joda.time.Instant}
*
* @param instant {@link java.time.Instant} 对象
* @return {@link org.joda.time.Instant} 对象
*/
public static org.joda.time.Instant toJodaInstant(java.time.Instant instant) {
return new org.joda.time.Instant(instant.toEpochMilli());
}
/**
* 将 {@link java.time.ZonedDateTime} 转换为 {@link org.joda.time.Instant}
*
* @param zonedDateTime {@link java.time.ZonedDateTime} 对象
* @return {@link org.joda.time.Instant} 对象
*/
public static org.joda.time.Instant toJodaInstant(java.time.ZonedDateTime zonedDateTime) {
return toJodaInstant(zonedDateTime.toInstant());
}
/**
* 计算指定时区的地区时间,对应的时间戳。结果为 {@link org.joda.time.Instant} 对象
*
* @param localDateTime {@link java.time.LocalDateTime} 对象
* @param zone 时区
* @return {@link org.joda.time.Instant} 对象
*/
public static org.joda.time.Instant toJodaInstant(
java.time.LocalDateTime localDateTime,
java.time.ZoneId zone) {
return toJodaInstant(java.time.ZonedDateTime.of(localDateTime, zone));
}
// ================================
// #endregion
// ================================
// ================================
// #region - toJavaInstant
// ================================
/**
* 将 {@link org.joda.time.Instant} 对象转换为 {@link java.time.Instant} 对象
*
* @param instant {@link org.joda.time.Instant} 对象
* @return {@link java.time.Instant} 对象
*/
public static java.time.Instant toJavaInstant(org.joda.time.Instant instant) {
return DateTimeTools.toInstant(instant.getMillis());
}
/**
* 将 joda-time 中的 {@link org.joda.time.DateTime} 对象转换为 Java 的
* {@link java.time.Instant} 对象
*
* @param dateTime joda-time 中表示日期时间的 {@link org.joda.time.DateTime} 对象
* @return Java 表示时间戳的 {@link java.time.Instant} 对象
*/
public static java.time.Instant toJavaInstant(org.joda.time.DateTime dateTime) {
return DateTimeTools.toInstant(dateTime.getMillis());
}
/**
* 将 joda-time 中的 {@link org.joda.time.LocalDateTime} 对象和
* {@link org.joda.time.DateTimeZone} 对象
* 转换为 Java 中的 {@link java.time.Instant} 对象
*
* @param localDateTime {@link org.joda.time.LocalDateTime} 对象
* @param zone {@link org.joda.time.DateTimeZone} 对象
* @return Java 表示时间戳的 {@link java.time.Instant} 对象
*/
public static java.time.Instant toJavaInstant(
org.joda.time.LocalDateTime localDateTime,
org.joda.time.DateTimeZone zone) {
return toJavaInstant(localDateTime.toDateTime(zone));
}
// ================================
// #endregion
// ================================
// ================================
// #region - toJodaDateTime
// ================================
/**
* 将 Java 中表示日期时间的 {@link java.time.ZonedDateTime} 对象
* 转换为 joda-time 的 {@link org.joda.time.DateTime} 对象
*
* @param zonedDateTime 日期时间
* @return joda-time 中对应的 {@link org.joda.time.DateTime} 对象
*/
public static org.joda.time.DateTime toJodaDateTime(java.time.ZonedDateTime zonedDateTime) {
org.joda.time.DateTimeZone zone = org.joda.time.DateTimeZone.forID(zonedDateTime.getZone().getId());
return toJodaInstant(zonedDateTime.toInstant()).toDateTime(zone);
}
/**
* 将 java.time 中表示日期时间的 {@link java.time.LocalDateTime} 对象和表示时区的
* {@link java.time.ZoneId} 对象转换为 joda-time 中对应的 {@link org.joda.time.DateTime}
* 对象
* 转换为 joda-time 中对应的 {@link org.joda.time.DateTime} 对象
*
* @param localDateTime 日期时间
* @param zone 时区
* @return joda-time 中对应的 {@link org.joda.time.DateTime} 对象
*/
public static org.joda.time.DateTime toJodaDateTime(
java.time.LocalDateTime localDateTime,
java.time.ZoneId zone) {
org.joda.time.LocalDateTime jodaLocalDateTime = toJodaLocalDateTime(localDateTime);
org.joda.time.DateTimeZone jodaZone = toJodaZone(zone);
return jodaLocalDateTime.toDateTime(jodaZone);
}
/**
* 计算时间戳在指定时区对应的时间,结果使用 {@link org.joda.time.DateTime} 表示
*
* @param instant java.time 中的时间戳
* @param zone java.time 中的时区
* @return joda-time 中带时区的日期时间
*/
public static org.joda.time.DateTime toJodaDateTime(
java.time.Instant instant,
java.time.ZoneId zone) {
org.joda.time.DateTimeZone dateTimeZone = toJodaZone(zone);
return toJodaInstant(instant).toDateTime(dateTimeZone);
}
// ================================
// #endregion
// ================================
// ================================
// #region - toZonedDateTime
// ================================
/**
* 将 joda-time 中带时区的日期时间,转换为 java.time 中带时区的日期时间
*
* @param dateTime joda-time 中带时区的日期时间
* @return java.time 中带时区的日期时间
*/
public static java.time.ZonedDateTime toZonedDateTime(org.joda.time.DateTime dateTime) {
java.time.ZoneId zone = dateTime.getZone().toTimeZone().toZoneId();
return toJavaInstant(dateTime.toInstant()).atZone(zone);
}
/**
* 将 joda-time 中的 {@link org.joda.time.LocalDateTime} 和
* {@link org.joda.time.DateTimeZone}
* 转换为 java.time 中的 {@link java.time.ZonedDateTime}
*
* @param localDateTime joda-time 中的地区时间
* @param dateTimeZone joda-time 中的时区
* @return java.time 中带时区的日期时间
*/
public static java.time.ZonedDateTime toZonedDateTime(
org.joda.time.LocalDateTime localDateTime,
org.joda.time.DateTimeZone dateTimeZone) {
java.time.ZoneId zone = toJavaZone(dateTimeZone);
return toJavaInstant(localDateTime, dateTimeZone).atZone(zone);
}
/**
* 获取 joda-time 中的 {@link org.joda.time.Instant} 在指定时区的时间,用 Java 8+ 的
* {@link java.time.ZonedDateTime} 表示
*
* @param instant joda-time 中的时间戳
* @param dateTimeZone joda-time 中的时区
* @return java.time 中带时区的日期时间
*/
public static java.time.ZonedDateTime toZonedDateTime(
org.joda.time.Instant instant,
org.joda.time.DateTimeZone dateTimeZone) {
java.time.ZoneId zone = toJavaZone(dateTimeZone);
return toJavaInstant(instant).atZone(zone);
}
// ================================
// #endregion
// ================================
// ================================
// #region - toJodaLocalDateTime
// ================================
/**
* 将 {@link java.time.LocalDateTime} 转换为 {@link org.joda.time.LocalDateTime}
*
* @param localDateTime Java 8 LocalDateTime
* @return joda-time LocalDateTime
*/
public static org.joda.time.LocalDateTime toJodaLocalDateTime(java.time.LocalDateTime localDateTime) {
return new org.joda.time.LocalDateTime(
localDateTime.getYear(),
localDateTime.getMonthValue(),
localDateTime.getDayOfMonth(),
localDateTime.getHour(),
localDateTime.getMinute(),
localDateTime.getSecond(),
localDateTime.getNano() / 1_000_000 // 毫秒转纳秒
);
}
// ================================
// #endregion
// ================================
// ================================
// #region - toJavaLocalDateTime
// ================================
/**
* 将 {@link org.joda.time.LocalDateTime} 转换为 {@link java.time.LocalDateTime}
*
* @param localDateTime joda-time LocalDateTime
* @return Java 8 LocalDateTime
*/
public static java.time.LocalDateTime toJavaLocalDateTime(org.joda.time.LocalDateTime localDateTime) {
return java.time.LocalDateTime.of(
localDateTime.getYear(),
localDateTime.getMonthOfYear(),
localDateTime.getDayOfMonth(),
localDateTime.getHourOfDay(),
localDateTime.getMinuteOfHour(),
localDateTime.getSecondOfMinute(),
localDateTime.getMillisOfSecond() * 1_000_000 // 毫秒转纳秒
);
}
// ================================
// #endregion
// ================================
// ================================
// #region - ZoneId <--> DateTimeZone
// ================================
/**
* 转换 Java API 和 joda-time API 表示时区的对象
*
* @param jodaZone joda-time API 中表示时区的对象
* @return Java API 中表示时区的对象
*/
public static java.time.ZoneId toJavaZone(org.joda.time.DateTimeZone jodaZone) {
return jodaZone.toTimeZone().toZoneId();
}
/**
* 转换 Java API 和 joda-time API 表示时区的对象
*
* @param zone Java API 中表示时区的对象
* @return joda-time API 中表示时区的对象
*/
public static org.joda.time.DateTimeZone toJodaZone(java.time.ZoneId zone) {
return org.joda.time.DateTimeZone.forID(zone.getId());
}
// ================================
// #endregion
// ================================
/**
* 私有构造方法
*/
private JodaTimeTools() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,346 @@
/*
* Copyright 2022-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import java.math.BigDecimal;
import java.math.BigInteger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* 数字工具类
*
* @author ZhouXY
*/
public class Numbers {
// ================================
// #region - sum
// ================================
/**
* 求和
*
* @param numbers 数据数组
* @return 求和结果
*/
public static int sum(final short... numbers) {
int result = 0;
for (short number : numbers) {
result += number;
}
return result;
}
/**
* 求和
*
* @param numbers 数据数组
* @return 求和结果
*/
public static long sum(final int... numbers) {
long result = 0L;
for (int number : numbers) {
result += number;
}
return result;
}
/**
* 求和
*
* @param numbers 数据数组
* @return 求和结果
*/
public static long sum(final long... numbers) {
long result = 0L;
for (long number : numbers) {
result += number;
}
return result;
}
/**
* 求和
*
* @param numbers 数据数组
* @return 求和结果
*/
public static double sum(final float... numbers) {
double result = 0.00;
for (float number : numbers) {
result += number;
}
return result;
}
/**
* 求和
*
* @param numbers 数据数组
* @return 求和结果
*/
public static double sum(final double... numbers) {
double result = 0.00;
for (double number : numbers) {
result += number;
}
return result;
}
/**
* 求和
*
* @param numbers 数据数组
* @return 求和结果
*/
public static BigInteger sum(final BigInteger... numbers) {
if (ArrayTools.isEmpty(numbers)) {
return BigInteger.ZERO;
}
BigInteger result = Numbers.nullToZero(numbers[0]);
for (int i = 1; i < numbers.length; i++) {
BigInteger value = numbers[i];
if (value != null) {
result = result.add(value);
}
}
return result;
}
/**
* 求和
*
* @param numbers 数据数组
* @return 求和结果
*/
public static BigDecimal sum(final BigDecimal... numbers) {
return BigDecimals.sum(numbers);
}
// ================================
// #endregion
// ================================
// ================================
// #region - nullToZero
// ================================
/**
* 将 {@code null} 转换为 {@code 0}
*
* @param val 待转换的值
* @return 如果 {@code val} 不为 {@code null},则返回该值;如果值为 {@code null},则返回 {@code 0}
*/
public static byte nullToZero(@Nullable final Byte val) {
return val != null ? val : 0;
}
/**
* 将 {@code null} 转换为 {@code 0}
*
* @param val 待转换的值
* @return 如果 {@code val} 不为 {@code null},则返回该值;如果值为 {@code null},则返回 {@code 0}
*/
public static short nullToZero(@Nullable final Short val) {
return val != null ? val : 0;
}
/**
* 将 {@code null} 转换为 {@code 0}
*
* @param val 待转换的值
* @return 如果 {@code val} 不为 {@code null},则返回该值;如果值为 {@code null},则返回 {@code 0}
*/
public static int nullToZero(@Nullable final Integer val) {
return val != null ? val : 0;
}
/**
* 将 {@code null} 转换为 {@code 0}
*
* @param val 待转换的值
* @return 如果 {@code val} 不为 {@code null},则返回该值;如果值为 {@code null},则返回 {@code 0}
*/
public static long nullToZero(@Nullable final Long val) {
return val != null ? val : 0L;
}
/**
* 将 {@code null} 转换为 {@code 0}
*
* @param val 待转换的值
* @return 如果 {@code val} 不为 {@code null},则返回该值;如果值为 {@code null},则返回 {@code 0}
*/
public static float nullToZero(@Nullable final Float val) {
return val != null ? val : 0.0F;
}
/**
* 将 {@code null} 转换为 {@code 0}
*
* @param val 待转换的值
* @return 如果 {@code val} 不为 {@code null},则返回该值;如果值为 {@code null},则返回 {@code 0}
*/
public static double nullToZero(@Nullable final Double val) {
return val != null ? val : 0.0;
}
/**
* 将 {@code null} 转换为 {@code 0}
*
* @param val 待转换的值
* @return 如果 {@code val} 不为 {@code null},则返回该值;如果值为 {@code null},则返回 {@code 0}
*/
@Nonnull
public static BigInteger nullToZero(@Nullable final BigInteger val) {
return val != null ? val : BigInteger.ZERO;
}
/**
* 将 {@code null} 转换为 {@code 0}
*
* @param val 待转换的值
* @return 如果 {@code val} 不为 {@code null},则返回该值;如果值为 {@code null},则返回 {@code 0}
*/
@Nonnull
public static BigDecimal nullToZero(@Nullable final BigDecimal val) {
return BigDecimals.nullToZero(val);
}
// ================================
// #endregion - nullToZero
// ================================
// ================================
// #region - parse
// ================================
/**
* 将字符串转为对应 {@link Short},转换失败时返回 {@code defaultValue}(允许为 {@code null})。
*
* @param str 要转换的字符串
* @param defaultValue 默认值
* @return 转换结果
*/
@Nullable
public static Short parseShort(@Nullable String str, @Nullable Short defaultValue) {
if (StringTools.isBlank(str)) {
return defaultValue;
}
try {
return Short.parseShort(str);
}
catch (NumberFormatException ignore) {
// ignore
}
return defaultValue;
}
/**
* 将字符串转为 {@link Integer},转换失败时返回 {@code defaultValue}(允许为 {@code null})。
*
* @param str 要转换的字符串
* @param defaultValue 默认值
* @return 转换结果
*/
@Nullable
public static Integer parseInteger(@Nullable String str, @Nullable Integer defaultValue) {
if (StringTools.isBlank(str)) {
return defaultValue;
}
try {
return Integer.parseInt(str);
}
catch (NumberFormatException ignore) {
// ignore
}
return defaultValue;
}
/**
* 将字符串转为 {@link Long},转换失败时返回 {@code defaultValue}(允许为 {@code null})。
*
* @param str 要转换的字符串
* @param defaultValue 默认值
* @return 转换结果
*/
@Nullable
public static Long parseLong(@Nullable String str, @Nullable Long defaultValue) {
if (StringTools.isBlank(str)) {
return defaultValue;
}
try {
return Long.parseLong(str);
}
catch (NumberFormatException ignore) {
// ignore
}
return defaultValue;
}
/**
* 将字符串转为 {@link Float},转换失败时返回 {@code defaultValue}(允许为 {@code null})。
*
* @param str 要转换的字符串
* @param defaultValue 默认值
* @return 转换结果
*/
@Nullable
public static Float parseFloat(@Nullable String str, @Nullable Float defaultValue) {
if (StringTools.isBlank(str)) {
return defaultValue;
}
try {
return Float.parseFloat(str);
}
catch (NumberFormatException ignore) {
// ignore
}
return defaultValue;
}
/**
* 将字符串转为 {@link Double},转换失败时返回 {@code defaultValue}(允许为 {@code null})。
*
* @param str 要转换的字符串
* @param defaultValue 默认值
* @return 转换结果
*/
@Nullable
public static Double parseDouble(@Nullable String str, @Nullable Double defaultValue) {
if (StringTools.isBlank(str)) {
return defaultValue;
}
try {
return Double.parseDouble(str);
}
catch (NumberFormatException ignore) {
// ignore
}
return defaultValue;
}
// ================================
// #endregion - parse
// ================================
private Numbers() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 the original author or authors.
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,27 +26,26 @@ import javax.annotation.Nullable;
import com.google.common.annotations.Beta;
/**
* OptionalUtil
* OptionalTools
*
* <p>
* 提供一些 Optional 相关的方法
*
* @author <a href="https://gitee.com/zhouxy108">ZhouXY</a>
* @since 0.1.0
* @author ZhouXY
* @since 1.0.0
* @see Optional
* @see OptionalInt
* @see OptionalLong
* @see OptionalDouble
*/
public class OptionalUtil {
public class OptionalTools {
/**
* 将包装类 {@link Integer} 转为 {@link OptionalInt}not null
* <p>
* 包装类为 {@code null} 表示值的缺失转为 {@link OptionalInt}
* {@link OptionalInt#empty()} 表示值的缺失
* </p>
*
*
* @param value 包装对象
* @return {@link OptionalInt} 实例
*/
@@ -58,13 +57,12 @@ public class OptionalUtil {
* {@code Optional<Integer>} 对象转为 {@link OptionalInt} 对象
* <p>
* {@code Optional<Integer>} 将整数包装了两次改为使用 {@link OptionalInt} 包装其中的整数数据
* </p>
*
*
* @param optionalObj {@code Optional<Integer>} 对象
* @return {@link OptionalInt} 实例
*/
public static OptionalInt toOptionalInt(Optional<Integer> optionalObj) {
return optionalOf(optionalObj.orElse(null));
return optionalObj.map(OptionalInt::of).orElseGet(OptionalInt::empty);
}
/**
@@ -72,8 +70,7 @@ public class OptionalUtil {
* <p>
* 包装类为 {@code null} 表示值的缺失转为 {@link OptionalLong}
* {@link OptionalLong#empty()} 表示值的缺失
* </p>
*
*
* @param value 包装对象
* @return {@link OptionalLong} 实例
*/
@@ -85,13 +82,12 @@ public class OptionalUtil {
* {@code Optional<Long>} 转为 {@link OptionalLong}
* <p>
* {@code Optional<Long>} 将整数包装了两次改为使用 {@link OptionalLong} 包装其中的整数数据
* </p>
*
*
* @param optionalObj 包装对象
* @return {@link OptionalLong} 实例
*/
public static OptionalLong toOptionalLong(Optional<Long> optionalObj) {
return optionalOf(optionalObj.orElse(null));
return optionalObj.map(OptionalLong::of).orElseGet(OptionalLong::empty);
}
/**
@@ -99,8 +95,7 @@ public class OptionalUtil {
* <p>
* 包装类为 {@code null} 表示值的缺失转为 {@link OptionalDouble}
* {@link OptionalDouble#empty()} 表示值的缺失
* </p>
*
*
* @param value 包装对象
* @return {@link OptionalDouble} 实例
*/
@@ -112,44 +107,65 @@ public class OptionalUtil {
* {@code Optional<Double>} 转为 {@link OptionalDouble}
* <p>
* {@code Optional<Double>} 将整数包装了两次改为使用 {@link OptionalDouble} 包装其中的整数数据
* </p>
*
*
* @param optionalObj 包装对象
* @return {@link OptionalDouble} 实例
*/
public static OptionalDouble toOptionalDouble(Optional<Double> optionalObj) {
return optionalOf(optionalObj.orElse(null));
return optionalObj.map(OptionalDouble::of).orElseGet(OptionalDouble::empty);
}
/**
* return the value of the optional object if present,
* otherwise {@code null}.
*
*
* @param <T> the class of the value
* @param optionalObj {@link Optional} object, which must be non-null.
* @return the value of the optional object if present, otherwise {@code null}.
*/
@Beta
@Nullable
public static <T> T orElseNull(Optional<T> optionalObj) {
return optionalObj.orElse(null);
}
/**
* {@link OptionalInt} 转为 {@link Integer}
*
* @param optionalObj optional 对象
* @return {@link Integer} 对象如果 {@code OptionalInt} 的值缺失返回 {@code null}
*/
@Beta
@Nullable
public static Integer toInteger(OptionalInt optionalObj) {
return optionalObj.isPresent() ? optionalObj.getAsInt() : null;
}
/**
* {@link OptionalLong} 转为 {@link Long}
*
* @param optionalObj optional 对象
* @return {@link Long} 对象如果 {@code OptionalLong} 的值缺失返回 {@code null}
*/
@Beta
@Nullable
public static Long toLong(OptionalLong optionalObj) {
return optionalObj.isPresent() ? optionalObj.getAsLong() : null;
}
/**
* {@link OptionalDouble} 转为 {@link Double}
*
* @param optionalObj optional 对象
* @return {@link Double} 对象如果 {@code OptionalDouble} 的值缺失返回 {@code null}
*/
@Beta
@Nullable
public static Double toDouble(OptionalDouble optionalObj) {
return optionalObj.isPresent() ? optionalObj.getAsDouble() : null;
}
private OptionalUtil() {
private OptionalTools() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,346 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
/**
* 随机工具类
*
* @author ZhouXY
*/
public final class RandomTools {
private static final SecureRandom DEFAULT_SECURE_RANDOM;
static {
SecureRandom secureRandom;
try {
secureRandom = SecureRandom.getInstanceStrong(); // 获取高强度安全随机数生成器
}
catch (NoSuchAlgorithmException e) {
secureRandom = new SecureRandom(); // 获取普通的安全随机数生成器
}
DEFAULT_SECURE_RANDOM = secureRandom;
}
/**
* 大写字母
*/
public static final String CAPITAL_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
* 小写字母
*/
public static final String LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz";
/**
* 数字
*/
public static final String NUMBERS = "0123456789";
/**
* 默认的 {@code SecureRandom}
*
* @return 默认的 {@code SecureRandom}
*/
public static SecureRandom defaultSecureRandom() {
return DEFAULT_SECURE_RANDOM;
}
/**
* 当前线程的 {@code ThreadLocalRandom}
*
* @return 当前线程的 {@code ThreadLocalRandom}
*/
public static ThreadLocalRandom currentThreadLocalRandom() {
return ThreadLocalRandom.current();
}
// ================================
// #region - randomStr
// ================================
/**
* 使用传入的随机数生成器,生成指定长度的字符串
*
* @param random 随机数生成器。根据需要可以传入
* {@link java.util.concurrent.ThreadLocalRandom}、{@link java.security.SecureRandom}
* 等,不为空
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String randomStr(Random random, char[] sourceCharacters, int length) {
checkArgumentNotNull(random, "Random cannot be null.");
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
return randomStrInternal(random, sourceCharacters, length);
}
/**
* 使用当前线程的 {@code ThreadLocalRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String randomStr(char[] sourceCharacters, int length) {
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
return randomStrInternal(ThreadLocalRandom.current(), sourceCharacters, length);
}
/**
* 使用默认的 {@code SecureRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String secureRandomStr(char[] sourceCharacters, int length) {
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
return randomStrInternal(DEFAULT_SECURE_RANDOM, sourceCharacters, length);
}
/**
* 使用传入的随机数生成器,生成指定长度的字符串
*
* @param random 随机数生成器。根据需要可以传入
* {@link java.util.concurrent.ThreadLocalRandom}、{@link java.security.SecureRandom}
* 等,不为空
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String randomStr(Random random, String sourceCharacters, int length) {
checkArgumentNotNull(random, "Random cannot be null.");
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
return randomStrInternal(random, sourceCharacters, length);
}
/**
* 使用当前线程的 {@code ThreadLocalRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String randomStr(String sourceCharacters, int length) {
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
return randomStrInternal(ThreadLocalRandom.current(), sourceCharacters, length);
}
/**
* 使用默认的 {@code SecureRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String secureRandomStr(String sourceCharacters, int length) {
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
return randomStrInternal(DEFAULT_SECURE_RANDOM, sourceCharacters, length);
}
// ================================
// #endregion - randomStr
// ================================
// ================================
// #region - randomInt
// ================================
/**
* 使用传入的随机数生成器,生成随机整数
*
* @param random 随机数生成器。根据需要传入
* @param startInclusive 最小值(包含)
* @param endExclusive 最大值(不包含)
* @return 在区间 {@code [min, max)} 内的随机整数
*
* @since 1.1.0
*/
public static int randomInt(Random random, int startInclusive, int endExclusive) {
checkArgumentNotNull(random, "Random cannot be null.");
checkArgument(startInclusive < endExclusive, "Start value must be less than end value.");
return randomIntInternal(random, startInclusive, endExclusive);
}
/**
* 使用当前线程的 {@code ThreadLocalRandom},生成随机整数
*
* @param startInclusive 最小值(包含)
* @param endExclusive 最大值(不包含)
* @return 在区间 {@code [min, max)} 内的随机整数
*
* @since 1.1.0
*/
public static int randomInt(int startInclusive, int endExclusive) {
checkArgument(startInclusive < endExclusive, "Start value must be less than end value.");
return randomIntInternal(ThreadLocalRandom.current(), startInclusive, endExclusive);
}
/**
* 使用默认的 {@code SecureRandom},生成随机整数
*
* @param startInclusive 最小值(包含)
* @param endExclusive 最大值(不包含)
* @return 在区间 {@code [min, max)} 内的随机整数
*
* @since 1.1.0
*/
public static int secureRandomInt(int startInclusive, int endExclusive) {
checkArgument(startInclusive < endExclusive, "Start value must be less than end value.");
return randomIntInternal(DEFAULT_SECURE_RANDOM, startInclusive, endExclusive);
}
/**
* 使用传入的随机数生成器,生成随机整数
*
* @param random 随机数生成器
* @param range 整数区间
* @return 在指定区间内的随机整数
*
* @since 1.1.0
*/
public static int randomInt(Random random, Range<Integer> range) {
checkArgumentNotNull(random, "Random cannot be null.");
checkArgumentNotNull(range, "Range cannot be null.");
return randomIntInternal(random, range);
}
/**
* 使用当前线程的 {@code ThreadLocalRandom},生成随机整数
*
* @param range 整数区间
* @return 在指定区间内的随机整数
*
* @since 1.1.0
*/
public static int randomInt(Range<Integer> range) {
checkArgumentNotNull(range, "Range cannot be null.");
return randomIntInternal(ThreadLocalRandom.current(), range);
}
/**
* 使用默认的 {@code SecureRandom},生成随机整数
*
* @param range 整数区间
* @return 在指定区间内的随机整数
*
* @since 1.1.0
*/
public static int secureRandomInt(Range<Integer> range) {
checkArgumentNotNull(range, "Range cannot be null.");
return randomIntInternal(DEFAULT_SECURE_RANDOM, range);
}
// ================================
// #endregion - randomInt
// ================================
// ================================
// #region - private methods
// ================================
/**
* 使用传入的随机数生成器,生成指定长度的字符串
*
* @param random 随机数生成器。根据需要可以传入
* {@link java.util.concurrent.ThreadLocalRandom}、{@link java.security.SecureRandom}
* 等,不为空
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
private static String randomStrInternal(Random random, char[] sourceCharacters, int length) {
if (length == 0) {
return StringTools.EMPTY_STRING;
}
final char[] result = new char[length];
for (int i = 0; i < length; i++) {
result[i] = sourceCharacters[random.nextInt(sourceCharacters.length)];
}
return String.valueOf(result);
}
/**
* 使用传入的随机数生成器,生成指定长度的字符串
*
* @param random 随机数生成器。根据需要可以传入
* {@link java.util.concurrent.ThreadLocalRandom}、{@link java.security.SecureRandom}
* 等,不为空
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
private static String randomStrInternal(Random random, String sourceCharacters, int length) {
if (length == 0) {
return StringTools.EMPTY_STRING;
}
final char[] result = new char[length];
for (int i = 0; i < length; i++) {
result[i] = sourceCharacters.charAt(random.nextInt(sourceCharacters.length()));
}
return String.valueOf(result);
}
/**
* 使用传入的随机数生成器,生成随机整数
*
* @param startInclusive 最小值(包含)
* @param endExclusive 最大值(不包含)
* @return 在区间 {@code [min, max)} 内的随机整数
*/
private static int randomIntInternal(Random random, int startInclusive, int endExclusive) {
return random.nextInt(endExclusive - startInclusive) + startInclusive;
}
/**
* 使用传入的随机数生成器,生成随机整数
*
* @param range 整数区间
* @return 在指定区间内的随机整数
*/
private static int randomIntInternal(Random random, Range<Integer> range) {
Integer lowerEndpoint = range.lowerEndpoint();
Integer upperEndpoint = range.upperEndpoint();
int min = range.lowerBoundType() == BoundType.CLOSED ? lowerEndpoint : lowerEndpoint + 1;
int max = range.upperBoundType() == BoundType.OPEN ? upperEndpoint : upperEndpoint + 1;
return random.nextInt(max - min) + min;
}
// ================================
// #endregion - private methods
// ================================
private RandomTools() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,205 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import javax.annotation.Nullable;
/**
* {@link Ref} 包装了一个值,表示对该值的应用。
*
* <p>灵感来自于 C# 的 {@code ref} 参数修饰符。C# 允许通过以下方式,将值返回给调用端:</p>
* <pre>
* void Method(ref int refArgument)
* {
* refArgument = refArgument + 44;
* }
*
* int number = 1;
* Method(ref number);
* Console.WriteLine(number); // Output: 45
* </pre>
* {@link Ref} 使 Java 可以达到类似的效果,如:
* <pre>
* void method(final Ref&lt;Integer&gt; refArgument) {
* refArgument.transformValue(i -&gt; i + 44);
* }
*
* Ref&lt;Integer&gt; number = Ref.of(1);
* method(number);
* System.out.println(number.getValue()); // Output: 45
* </pre>
* <p>
* 当一个方法需要产生多个结果时,无法有多个返回值,可以使用 {@link Ref} 作为参数传入,方法内部修改 {@link Ref} 的值。
* 调用方在调用方法之后,使用 {@code getValue()} 获取结果。
*
* <pre>
* String method(final Ref&lt;Integer&gt; intRefArgument, final Ref&lt;String&gt; strRefArgument) {
* intRefArgument.transformValue(i -&gt; i + 44);
* strRefArgument.setValue("Hello " + strRefArgument.getValue());
* return "Return string";
* }
*
* Ref&lt;Integer&gt; number = Ref.of(1);
* Ref&lt;String&gt; str = Ref.of("Java");
* String result = method(number, str);
* System.out.println(number.getValue()); // Output: 45
* System.out.println(str.getValue()); // Output: Hello Java
* System.out.println(result); // Output: Return string
* </pre>
*
* @author ZhouXY
* @since 1.0.0
*/
public final class Ref<T> {
@Nullable
private T value;
private Ref(@Nullable T value) {
this.value = value;
}
/**
* 创建对象引用
*
* @param <T> 引用的类型
* @param value 引用的对象
* @return {@code Ref} 对象
*/
public static <T> Ref<T> of(@Nullable T value) {
return new Ref<>(value);
}
/**
* 创建空引用
*
* @param <T> 引用的类型
* @return 空引用
*/
public static <T> Ref<T> empty() {
return new Ref<>(null);
}
/**
* 获取引用的对象
*
* @return 引用的对象
*/
@Nullable
public T getValue() {
return value;
}
/**
* 设置引用的对象
*
* @param value 要引用的对象
*/
public void setValue(@Nullable T value) {
this.value = value;
}
/**
* 使用 {@link UnaryOperator} 修改 {@code Ref} 内部引用的对象
*
* @param operator 修改逻辑
*/
public void transformValue(UnaryOperator<T> operator) {
this.value = operator.apply(this.value);
}
/**
* 使用 {@link Function} 修改所引用的对象,返回新的 {@code Ref}
*
* @param <R> 结果的引用类型
* @param function 修改逻辑
* @return 修改后的对象的引用
*/
public <R> Ref<R> transform(Function<? super T, R> function) {
return Ref.of(function.apply(this.value));
}
/**
* 使用 {@link Predicate} 检查引用的对象
*
* @param predicate 判断逻辑
* @return 判断结果
*/
public boolean checkValue(Predicate<? super T> predicate) {
return predicate.test(this.value);
}
/**
* 将引用的对象作为入参,执行 {@link Consumer} 的逻辑
*
* @param consumer 要执行的逻辑
*/
public void execute(Consumer<? super T> consumer) {
consumer.accept(value);
}
/**
* 判断所引用的对象是否为 {@code null}
*
* @return 是否为 {@code null}
*/
public boolean isNull() {
return this.value == null;
}
/**
* 判断所引用的对象是否不为 {@code null}
*
* @return 是否不为 {@code null}
*/
public boolean isNotNull() {
return this.value != null;
}
@Override
public String toString() {
return String.format("Ref[%s]", value);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((value == null) ? 0 : value.hashCode());
return result;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Ref<?> other = (Ref<?>) obj;
return Objects.equals(this.value, other.value);
}
}

View File

@@ -0,0 +1,389 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkNotNull;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
/**
* 封装一些常用的正则操作,并可以缓存 {@link Pattern} 实例以复用。
*
* @author ZhouXY
*/
public final class RegexTools {
private static final int MAX_CACHE_SIZE = 256;
private static final int DEFAULT_FLAG = 0;
private static final LoadingCache<RegexAndFlags, Pattern> PATTERN_CACHE = CacheBuilder
.newBuilder()
.maximumSize(MAX_CACHE_SIZE)
.build(new CacheLoader<RegexAndFlags, Pattern>() {
@SuppressWarnings("null")
public Pattern load(RegexAndFlags regexAndFlags) {
return regexAndFlags.compilePattern();
}
});
// ================================
// #region - getPattern
// ================================
/**
* 获取 {@link Pattern} 实例。
*
* @param pattern 正则表达式
* @param cachePattern 是否缓存 {@link Pattern} 实例
* @return {@link Pattern} 实例
*/
public static Pattern getPattern(final String pattern, final boolean cachePattern) {
return getPattern(pattern, DEFAULT_FLAG, cachePattern);
}
/**
* 获取 {@link Pattern} 实例。
*
* @param pattern 正则表达式
* @param flags 正则表达式匹配标识
* @param cachePattern 是否缓存 {@link Pattern} 实例
* @return {@link Pattern} 实例
*/
public static Pattern getPattern(final String pattern, final int flags, final boolean cachePattern) {
checkNotNull(pattern);
return cachePattern ? cacheAndGetPatternInternal(pattern, flags) : getPatternInternal(pattern, flags);
}
/**
* 获取 {@link Pattern} 实例,不缓存。
*
* @param pattern 正则表达式
* @return {@link Pattern} 实例
*/
public static Pattern getPattern(final String pattern) {
return getPattern(pattern, DEFAULT_FLAG);
}
/**
* 获取 {@link Pattern} 实例,不缓存。
*
* @param pattern 正则表达式
* @param flags 正则表达式匹配标识
* @return {@link Pattern} 实例
*/
@Nonnull
public static Pattern getPattern(final String pattern, final int flags) {
checkNotNull(pattern);
return getPatternInternal(pattern, flags);
}
// ================================
// #endregion - getPattern
// ================================
// ================================
// #region - matches
// ================================
/**
* 判断 {@code input} 是否匹配 {@code pattern}。
*
* @param input 输入
* @param pattern 正则
* @return 判断结果
*/
public static boolean matches(@Nullable final CharSequence input, final Pattern pattern) {
checkNotNull(pattern);
return matchesInternal(input, pattern);
}
/**
* 判断 {@code input} 是否匹配 {@code patterns} 中的一个。
*
* @param input 输入
* @param patterns 正则
* @return 判断结果
*/
public static boolean matchesAny(@Nullable final CharSequence input, final Pattern[] patterns) {
checkArgument(ArrayTools.isAllElementsNotNull(patterns));
return matchesAnyInternal(input, patterns);
}
/**
* 判断 {@code input} 是否匹配全部正则。
*
* @param input 输入
* @param patterns 正则
* @return 判断结果
*/
public static boolean matchesAll(@Nullable final CharSequence input, final Pattern[] patterns) {
checkArgument(ArrayTools.isAllElementsNotNull(patterns));
return matchesAllInternal(input, patterns);
}
/**
* 判断 {@code input} 是否匹配 {@code pattern}。
*
* @param input 输入
* @param pattern 正则表达式
* @param cachePattern 是否缓存 {@link Pattern} 实例
* @return 判断结果
*/
public static boolean matches(@Nullable final CharSequence input, final String pattern,
final boolean cachePattern) {
return matches(input, pattern, DEFAULT_FLAG, cachePattern);
}
/**
* 判断 {@code input} 是否匹配 {@code pattern}。
*
* @param input 输入
* @param pattern 正则表达式
* @param flags 正则表达式匹配标识
* @param cachePattern 是否缓存 {@link Pattern} 实例
* @return 判断结果
*/
public static boolean matches(@Nullable final CharSequence input, final String pattern, final int flags,
final boolean cachePattern) {
return matchesInternal(input, getPattern(pattern, flags, cachePattern));
}
/**
* 判断 {@code input} 是否匹配 {@code pattern}。不缓存 {@link Pattern} 实例。
*
* @param input 输入
* @param pattern 正则表达式
* @return 判断结果
*/
public static boolean matches(@Nullable final CharSequence input, final String pattern) {
return matches(input, pattern, DEFAULT_FLAG);
}
/**
* 判断 {@code input} 是否匹配 {@code pattern}。不缓存 {@link Pattern} 实例。
*
* @param input 输入
* @param pattern 正则表达式
* @param flags 正则表达式匹配标识
* @return 判断结果
*/
public static boolean matches(@Nullable final CharSequence input,
final String pattern, final int flags) {
return matchesInternal(input, getPattern(pattern, flags));
}
// ================================
// #endregion - matches
// ================================
// ================================
// #region - getMatcher
// ================================
/**
* 生成 Matcher。
*
* @param input 输入
* @param pattern 正则
* @return 结果
*/
public static Matcher getMatcher(final CharSequence input, final Pattern pattern) {
checkNotNull(input);
checkNotNull(pattern);
return pattern.matcher(input);
}
/**
* 生成 Matcher。
*
* @param input 输入
* @param pattern 正则表达式
* @param cachePattern 是否缓存 {@link Pattern} 实例
* @return 结果
*/
public static Matcher getMatcher(final CharSequence input, final String pattern, boolean cachePattern) {
return getMatcher(input, pattern, DEFAULT_FLAG, cachePattern);
}
/**
* 生成 Matcher。
*
* @param input 输入
* @param pattern 正则表达式
* @param flags 正则表达式匹配标识
* @param cachePattern 是否缓存 {@link Pattern} 实例
* @return 结果
*/
public static Matcher getMatcher(final CharSequence input,
final String pattern, final int flags, boolean cachePattern) {
return getMatcher(input, getPattern(pattern, flags, cachePattern));
}
/**
* 生成 Matcher。不缓存 {@link Pattern} 实例。
*
* @param input 输入
* @param pattern 正则表达式
* @return 结果
*/
public static Matcher getMatcher(final CharSequence input, final String pattern) {
return getMatcher(input, pattern, DEFAULT_FLAG);
}
/**
* 生成 Matcher。不缓存 {@link Pattern} 实例。
*
* @param input 输入
* @param pattern 正则表达式
* @param flags 正则表达式匹配标识
* @return 结果
*/
public static Matcher getMatcher(final CharSequence input, final String pattern, final int flags) {
checkNotNull(input);
checkNotNull(pattern);
return getPatternInternal(pattern, flags).matcher(input);
}
// ================================
// #endregion - getMatcher
// ================================
// ================================
// #region - internal methods
// ================================
/**
* 获取 {@link Pattern} 实例。
*
* @param pattern 正则表达式
* @param flags 正则表达式匹配标识
* @return {@link Pattern} 实例
*/
@Nonnull
private static Pattern cacheAndGetPatternInternal(final String pattern, final int flags) {
final RegexAndFlags regexAndFlags = new RegexAndFlags(pattern, flags);
return PATTERN_CACHE.getUnchecked(regexAndFlags);
}
/**
* 获取 {@link Pattern} 实例,不缓存。
*
* @param pattern 正则表达式
* @param flags 正则表达式匹配标识
* @return {@link Pattern} 实例
*/
@Nonnull
private static Pattern getPatternInternal(final String pattern, final int flags) {
final RegexAndFlags regexAndFlags = new RegexAndFlags(pattern, flags);
return Optional.ofNullable(PATTERN_CACHE.getIfPresent(regexAndFlags))
.orElseGet(regexAndFlags::compilePattern);
}
/**
* 判断 {@code input} 是否匹配 {@code pattern}。
*
* @param input 输入
* @param pattern 正则
* @return 判断结果
*/
private static boolean matchesInternal(@Nullable final CharSequence input, final Pattern pattern) {
return input != null && pattern.matcher(input).matches();
}
/**
* 判断 {@code input} 是否匹配至少一个正则。
*
* @param input 输入
* @param patterns 正则表达式
* @return 判断结果
*/
private static boolean matchesAnyInternal(@Nullable final CharSequence input, final Pattern[] patterns) {
return input != null
&& Arrays.stream(patterns)
.anyMatch(pattern -> pattern.matcher(input).matches());
}
/**
* 判断 {@code input} 是否匹配全部正则。
*
* @param input 输入
* @param patterns 正则表达式
* @return 判断结果
*/
private static boolean matchesAllInternal(@Nullable final CharSequence input, final Pattern[] patterns) {
return input != null
&& Arrays.stream(patterns)
.allMatch(pattern -> pattern.matcher(input).matches());
}
// ================================
// #endregion - internal methods
// ================================
private RegexTools() {
// 不允许实例化
throw new IllegalStateException("Utility class");
}
// ================================
// #region - RegexAndFlags
// ================================
private static final class RegexAndFlags {
private final String regex;
private final int flags;
private RegexAndFlags(String regex, int flags) {
this.regex = regex;
this.flags = flags;
}
private Pattern compilePattern() {
return Pattern.compile(regex, flags);
}
@Override
public int hashCode() {
return Objects.hash(regex, flags);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj)
return true;
if (!(obj instanceof RegexAndFlags))
return false;
RegexAndFlags other = (RegexAndFlags) obj;
return Objects.equals(regex, other.regex) && flags == other.flags;
}
}
// ================================
// #endregion - RegexAndFlags
// ================================
}

View File

@@ -1,14 +1,28 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import java.util.concurrent.TimeUnit;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
/**
* Twitter_Snowflake
* Twitter 版雪花算法
*/
@Beta
public class SnowflakeIdGenerator {
// ==============================Fields===========================================
@@ -52,9 +66,6 @@ public class SnowflakeIdGenerator {
/** 上次生成 ID 的时间截 */
private long lastTimestamp = -1L;
/** 锁对象 */
private final Object lock = new Object();
// ==============================Constructors=====================================
/**
@@ -64,9 +75,9 @@ public class SnowflakeIdGenerator {
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdGenerator(final long workerId, final long datacenterId) {
Preconditions.checkArgument((workerId > MAX_WORKER_ID || workerId < 0),
checkArgument((workerId <= MAX_WORKER_ID && workerId >= 0),
"WorkerId can't be greater than %s or less than 0.", MAX_WORKER_ID);
Preconditions.checkArgument((datacenterId > MAX_DATACENTER_ID || datacenterId < 0),
checkArgument((datacenterId <= MAX_DATACENTER_ID && datacenterId >= 0),
"DatacenterId can't be greater than %s or less than 0.", MAX_DATACENTER_ID);
this.datacenterIdAndWorkerId
= (datacenterId << DATACENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT);
@@ -78,51 +89,47 @@ public class SnowflakeIdGenerator {
*
* @return SnowflakeId
*/
public long nextId() {
long timestamp;
synchronized (lock) {
timestamp = timeGen();
public synchronized long nextId() {
long timestamp = timeGen();
// 发生了回拨此刻时间小于上次发号时间
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 时间偏差大小小于5ms则等待两倍时间
try {
TimeUnit.MILLISECONDS.sleep(offset << 1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
timestamp = timeGen();
if (timestamp < lastTimestamp) {
// 还是小于抛异常上报
throwClockBackwardsEx(lastTimestamp, timestamp);
}
} else {
// 发生了回拨此刻时间小于上次发号时间
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 时间偏差大小小于5ms则等待两倍时间
try {
TimeUnit.MILLISECONDS.sleep(offset << 1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
timestamp = timeGen();
if (timestamp < lastTimestamp) {
// 还是小于抛异常上报
throwClockBackwardsEx(lastTimestamp, timestamp);
}
} else {
throwClockBackwardsEx(lastTimestamp, timestamp);
}
// 如果是同一时间生成的则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变毫秒内序列重置
else {
sequence = 0L;
}
// 上次生成 ID 的时间截
lastTimestamp = timestamp;
}
// 如果是同一时间生成的则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变毫秒内序列重置
else {
sequence = 0L;
}
// 上次生成 ID 的时间截
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) | datacenterIdAndWorkerId | sequence;
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull;
import java.net.MalformedURLException;
import java.net.URL;
import javax.annotation.Nullable;
import com.google.common.annotations.Beta;
import xyz.zhouxy.plusone.commons.constant.PatternConsts;
/**
* StringTools
*
* <p>
* 字符串工具类。
*
* @author ZhouXY
* @since 1.0.0
*/
public class StringTools {
public static final String EMPTY_STRING = "";
/**
* 判断字符串是否非空白
*
* <pre>
* StringTools.isNotBlank(null); // false
* StringTools.isNotBlank(""); // false
* StringTools.isNotBlank(" "); // false
* StringTools.isNotBlank("Hello"); // true
* </pre>
*
* @param cs 检查的字符串
* @return 是否非空白
*/
public static boolean isNotBlank(@Nullable final String cs) {
if (cs == null || cs.isEmpty()) {
return false;
}
for (int i = 0; i < cs.length(); i++) {
if (!Character.isWhitespace(cs.charAt(i))) {
return true;
}
}
return false;
}
/**
* 判断是否空白字符串
*
* <pre>
* StringTools.isBlank(null); // true
* StringTools.isBlank(""); // true
* StringTools.isBlank(" "); // true
* StringTools.isBlank("Hello"); // false
* </pre>
*
* @param cs 检查的字符串
* @return 是否空白
* @since 1.1.0
*/
public static boolean isBlank(@Nullable String cs) {
if (cs == null || cs.isEmpty()) {
return true;
}
for (int i = 0; i < cs.length(); i++) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}
return true;
}
/**
* 重复字符串
*
* @param str 要重复的字符串
* @param times 重复次数
* @return 结果
*/
public static String repeat(String str, int times) {
return repeat(str, times, Integer.MAX_VALUE);
}
/**
* 重复字符串
*
* @param str 要重复的字符串
* @param times 重复次数
* @param maxLength 最大长度
* @return 结果
*/
public static String repeat(final String str, int times, int maxLength) {
checkArgumentNotNull(str);
return String.valueOf(ArrayTools.repeat(str.toCharArray(), times, maxLength));
}
/**
* 判断字符串是否非空
*
* <pre>
* StringTools.isNotEmpty(null); // false
* StringTools.isNotEmpty(""); // false
* StringTools.isNotEmpty(" "); // true
* StringTools.isNotEmpty("Hello"); // true
* </pre>
*
* @param cs 检查的字符串
* @return 是否非空
* @since 1.1.0
*/
public static boolean isNotEmpty(@Nullable final String cs) {
return cs != null && !cs.isEmpty();
}
/**
* 判断字符串是否为空字符串
*
* <pre>
* StringTools.isEmpty(null); // true
* StringTools.isEmpty(""); // true
* StringTools.isEmpty(" "); // false
* StringTools.isEmpty("Hello"); // false
* </pre>
*
* @param cs 检查的字符串
* @return 是否空字符串
* @since 1.1.0
*/
public static boolean isEmpty(@Nullable final String cs) {
return cs == null || cs.isEmpty();
}
/**
* 判断字符串是否为邮箱地址
*
* @param cs 检查的字符串
* @return 是否是邮箱地址
* @since 1.1.0
*
* @see PatternConsts#EMAIL
*/
@Beta
public static boolean isEmail(@Nullable final String cs) {
return RegexTools.matches(cs, PatternConsts.EMAIL);
}
/**
* 判断字符串是否为 URL 地址
*
* @param cs 检查的字符串
* @return 是否是 URL
* @since 1.1.0
*
* @see URL
*/
@Beta
public static boolean isURL(@Nullable final String cs) {
if (cs == null) {
return false;
}
try {
new URL(cs);
} catch (MalformedURLException e) {
return false;
}
return true;
}
/**
* 脱敏
*
* @param src 原字符串
* @param front 前面保留的字符数
* @param end 后面保留的字符数
* @return 脱敏结果
* @since 1.1.0
*/
public static String desensitize(@Nullable final String src, int front, int end) {
return desensitize(src, '*', front, end);
}
/**
* 脱敏
*
* @param src 原字符串
* @param replacedChar 用于替换的字符
* @param front 前面保留的字符数
* @param end 后面保留的字符数
* @return 脱敏结果
* @since 1.1.0
*/
public static String desensitize(@Nullable final String src, char replacedChar, int front, int end) {
if (src == null || src.isEmpty()) {
return EMPTY_STRING;
}
checkArgument(front >= 0 && end >= 0);
checkArgument((front + end) <= src.length(), "需要截取的长度不能大于原字符串长度");
final char[] charArray = src.toCharArray();
for (int i = front; i < charArray.length - end; i++) {
charArray[i] = replacedChar;
}
return String.valueOf(charArray);
}
/**
* 转换为带引号的字符串
*
* @param value 值
* @return 带引号的字符串
* @since 1.1.0
*/
public static String toQuotedString(@Nullable String value) {
if (value == null) {
return "null";
}
if (value.isEmpty()) {
return "\"\"";
}
return "\"" + value + "\"";
}
private StringTools() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2023-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkNotNull;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* TreeBuilder
*
* @author ZhouXY
* @since 1.0.0
*/
public class TreeBuilder<T, TSubTree extends T, TIdentity> {
private final Function<T, TIdentity> identityGetter;
private final Function<T, Optional<TIdentity>> parentIdentityGetter;
private final BiConsumer<TSubTree, T> addChildMethod;
private final Comparator<? super T> defaultComparator;
/**
* 构造一个 {@code TreeBuilder}。不指定用于排序的 {@code Comparator}。
*
* @param identityGetter 从节点中获取其标识的逻辑
* @param parentIdentityGetter 获取父节点标识的逻辑
* @param addChild 添加子节点的逻辑
*/
public TreeBuilder(Function<T, TIdentity> identityGetter, Function<T, Optional<TIdentity>> parentIdentityGetter,
BiConsumer<TSubTree, T> addChild) {
this(identityGetter, parentIdentityGetter, addChild, null);
}
/**
* 构造一个 {@code TreeBuilder}。
*
* @param identityGetter 从节点中获取其标识的逻辑
* @param parentIdentityGetter 获取父节点标识的逻辑
* @param addChild 添加子节点的逻辑
* @param defaultComparator 默认的 {@code Comparator},用于排序
*/
public TreeBuilder(Function<T, TIdentity> identityGetter, Function<T, Optional<TIdentity>> parentIdentityGetter,
BiConsumer<TSubTree, T> addChild, @Nullable Comparator<? super T> defaultComparator) {
this.identityGetter = identityGetter;
this.parentIdentityGetter = parentIdentityGetter;
this.addChildMethod = addChild;
this.defaultComparator = defaultComparator;
}
/**
* 将节点构建成树。使用 {@link #defaultComparator} 进行排序。如果 {@link #defaultComparator}
* <p>
* <b>注意,该方法会直接操作 nodes 列表中的节点,并没有做深拷贝,
* 注意避免 nodes 中的元素产生变化所带来的意料之外的影响。</b>
*
* @param nodes 平铺的节点列表
* @return 构造结果
*/
public List<T> buildTree(Collection<T> nodes) {
checkNotNull(nodes);
return buildTreeInternal(nodes, this.defaultComparator);
}
/**
* 将节点构建成树。
* <p>
* <b>!!注意:该方法会直接操作 nodes 列表中的节点,并没有做深拷贝,
* 注意避免 nodes 中的元素产生变化所带来的意料之外的影响。</b>
*
* @param nodes 平铺的节点列表
* @param comparator 用于节点的排序。
* 若为 {@code null},则使用 {@link #defaultComparator}
* 若 {@link #defaultComparator} 也为 {@code null},则不排序。
* <b>仅影响调用 addChild 的顺序,如果操作对象本身对应的控制了子节点的顺序,无法影响其相关逻辑。</b>
* @return 构建的树形结构
*/
public List<T> buildTree(Collection<T> nodes, @Nullable Comparator<? super T> comparator) {
checkNotNull(nodes);
final Comparator<? super T> c = (comparator != null) ? comparator : this.defaultComparator;
return buildTreeInternal(nodes, c);
}
/**
* 将节点构建成树。
* <p>
* <b>注意,该方法会直接操作 nodes 列表中的节点,并没有做深拷贝,
* 注意避免 nodes 中的元素产生变化所带来的意料之外的影响。</b>
*
* @param nodes 平铺的节点列表
* @param comparator 用于节点的排序。若为 {@code null},则不排序
*/
private List<T> buildTreeInternal(Collection<T> nodes, @Nullable Comparator<? super T> comparator) {
final Collection<T> allNodes;
if (comparator == null) {
allNodes = nodes;
} else {
allNodes = nodes.stream().sorted(comparator).collect(Collectors.toList());
}
final Map<TIdentity, T> identityNodeMap = allNodes.stream()
.collect(Collectors.toMap(identityGetter, Function.identity(), (n1, n2) -> n1));
// 根节点
final List<T> rootNodes = allNodes.stream()
.filter(node -> !this.parentIdentityGetter.apply(node).isPresent())
.collect(Collectors.toList());
allNodes.forEach(node -> parentIdentityGetter.apply(node).ifPresent(parentIdentity -> {
if (identityNodeMap.containsKey(parentIdentity)) {
@SuppressWarnings("unchecked")
TSubTree parentNode = (TSubTree) identityNodeMap.get(parentIdentity);
addChildMethod.accept(parentNode, node);
}
}));
return rootNodes;
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.util;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterOutputStream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
/**
* zip 工具类
*
* <p>
* 提供最基础的数据压缩/解压方法
*
* @author ZhouXY
*
* @see Deflater
* @see Inflater
*/
public class ZipTools {
/**
* 使用默认压缩级别压缩数据
*
* @param input 输入
* @return 压缩后的数据
*
* @throws IOException 发生 I/O 错误时抛出
*/
public static byte[] zip(@Nullable byte[] input) throws IOException {
return zipInternal(input, Deflater.DEFAULT_COMPRESSION);
}
/**
* 使用指定压缩级别压缩数据
*
* @param input 输入
* @param level 压缩级别
* @return 压缩后的数据
*
* @throws IOException 发生 I/O 错误时抛出
*/
public static byte[] zip(@Nullable byte[] input, int level) throws IOException {
checkArgument((level >= 0 && level <= 9) || level == Deflater.DEFAULT_COMPRESSION,
"invalid compression level");
return zipInternal(input, level);
}
@CheckForNull
private static byte[] zipInternal(@Nullable byte[] input, int level) throws IOException {
if (input == null) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (DeflaterOutputStream dos = new DeflaterOutputStream(out, new Deflater(level))) {
dos.write(input);
dos.finish();
return out.toByteArray();
}
}
/**
* 解压数据
*
* @param input 输入
* @return 解压后的数据
*
* @throws IOException 发生 I/O 错误时抛出
*/
@CheckForNull
public static byte[] unzip(@Nullable byte[] input) throws IOException {
if (input == null) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InflaterOutputStream dos = new InflaterOutputStream(out, new Inflater())) {
dos.write(input);
dos.finish();
return out.toByteArray();
}
}
private ZipTools() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* <h2>工具类</h2>
* <p>
* 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、
* ID 生成器({@link IdGenerator})及其它实用工具类。
*
* @author ZhouXY
*/
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.util;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons;
import static org.junit.jupiter.api.Assertions.assertSame;
@@ -11,6 +27,7 @@ import xyz.zhouxy.plusone.commons.util.Enumeration;
import java.util.ArrayList;
import java.util.Collection;
@SuppressWarnings("deprecation")
class EnumerationTests {
private static final Logger log = LoggerFactory.getLogger(EnumerationTests.class);
@@ -28,6 +45,7 @@ class EnumerationTests {
}
}
@SuppressWarnings("deprecation")
final class EntityStatus extends Enumeration<EntityStatus> {
private EntityStatus(int value, String name) {
@@ -38,7 +56,7 @@ final class EntityStatus extends Enumeration<EntityStatus> {
public static final EntityStatus AVAILABLE = new EntityStatus(0, "正常");
public static final EntityStatus DISABLED = new EntityStatus(1, "禁用");
private static final ValueSet<EntityStatus> VALUE_SET = ValueSet.of(AVAILABLE, DISABLED);
private static final ValueSet<EntityStatus> VALUE_SET = ValueSet.of(new EntityStatus[] { AVAILABLE, DISABLED });
public static EntityStatus of(int value) {
return VALUE_SET.get(value);
@@ -49,6 +67,7 @@ final class EntityStatus extends Enumeration<EntityStatus> {
}
}
@SuppressWarnings("deprecation")
final class Result extends Enumeration<Result> {
private Result(int id, String name) {
super(id, name);
@@ -57,7 +76,7 @@ final class Result extends Enumeration<Result> {
public static final Result SUCCESSFUL = new Result(1, "成功");
public static final Result FAILURE = new Result(0, "失败");
private static final ValueSet<Result> VALUE_SET = ValueSet.of(SUCCESSFUL, FAILURE);
private static final ValueSet<Result> VALUE_SET = ValueSet.of(new Result[] { SUCCESSFUL, FAILURE });
public static Result of(int id) {
return VALUE_SET.get(id);

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons;
import java.io.ObjectStreamClass;
import org.junit.jupiter.api.Test;
import lombok.extern.slf4j.Slf4j;
import xyz.zhouxy.plusone.commons.exception.system.NoAvailableMacFoundException;
@Slf4j
class SerialTests {
@Test
void testSerialVersionUID() {
long uid = getSerialVersionUID(NoAvailableMacFoundException.class);
log.info("\n private static final long serialVersionUID = {}L;", uid);
}
private long getSerialVersionUID(Class<?> cl) {
ObjectStreamClass c = ObjectStreamClass.lookup(cl);
return c.getSerialVersionUID();
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.base;
import static org.junit.jupiter.api.Assertions.*;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkNotNull;
import javax.annotation.Nonnull;
import org.junit.jupiter.api.Test;
class IWithCodeTests {
@Test
void equalsCode_SameCode_ReturnsTrue() {
assertTrue(WithCode.INSTANCE.isCodeEquals("testCode"));
Integer intCode = 0;
Long longCode = 0L;
assertTrue(WithIntCode.INSTANCE.isCodeEquals(intCode));
assertTrue(WithLongCode.INSTANCE.isCodeEquals(intCode));
assertTrue(WithLongCode.INSTANCE.isCodeEquals(longCode));
assertTrue(WithCode.INSTANCE.isSameCodeAs(WithCode.SAME_CODE_INSTANCE));
assertTrue(WithIntCode.INSTANCE.isSameCodeAs(WithIntCode.SAME_CODE_INSTANCE));
assertTrue(WithIntCode.INSTANCE.isSameCodeAs(WithLongCode.SAME_CODE_INSTANCE));
assertTrue(WithLongCode.INSTANCE.isSameCodeAs(WithLongCode.SAME_CODE_INSTANCE));
assertTrue(WithLongCode.INSTANCE.isSameCodeAs(WithIntCode.SAME_CODE_INSTANCE));
}
@Test
void equalsCode_DifferentCode_ReturnsFalse() {
assertFalse(WithCode.INSTANCE.isCodeEquals("wrongCode"));
Integer intCode = 108;
Long longCode = 108L;
assertFalse(WithIntCode.INSTANCE.isCodeEquals(intCode));
assertFalse(WithLongCode.INSTANCE.isCodeEquals(intCode));
assertFalse(WithLongCode.INSTANCE.isCodeEquals(longCode));
assertFalse(WithCode.INSTANCE.isSameCodeAs(WithCode.WRONG_CODE_INSTANCE));
assertFalse(WithIntCode.INSTANCE.isSameCodeAs(WithIntCode.WRONG_CODE_INSTANCE));
assertFalse(WithIntCode.INSTANCE.isSameCodeAs(WithLongCode.WRONG_CODE_INSTANCE));
assertFalse(WithLongCode.INSTANCE.isSameCodeAs(WithLongCode.WRONG_CODE_INSTANCE));
assertFalse(WithLongCode.INSTANCE.isSameCodeAs(WithIntCode.WRONG_CODE_INSTANCE));
}
@Test
@SuppressWarnings("null")
void equalsCode_NullCode_ReturnsFalse() {
assertFalse(WithCode.INSTANCE.isSameCodeAs((WithCode) null));
assertFalse(WithCode.INSTANCE.isSameCodeAs((WithIntCode) null));
assertFalse(WithCode.INSTANCE.isSameCodeAs((WithLongCode) null));
assertFalse(WithIntCode.INSTANCE.isSameCodeAs((WithCode) null));
assertFalse(WithIntCode.INSTANCE.isSameCodeAs((WithIntCode) null));
assertFalse(WithIntCode.INSTANCE.isSameCodeAs((WithLongCode) null));
assertFalse(WithLongCode.INSTANCE.isSameCodeAs((WithCode) null));
assertFalse(WithLongCode.INSTANCE.isSameCodeAs((WithIntCode) null));
assertFalse(WithLongCode.INSTANCE.isSameCodeAs((WithLongCode) null));
assertFalse(WithCode.INSTANCE.isCodeEquals((String) null));
Integer intCode = null;
Long longCode = null;
assertThrows(NullPointerException.class, () -> WithIntCode.INSTANCE.isCodeEquals(intCode));
assertThrows(NullPointerException.class, () -> WithLongCode.INSTANCE.isCodeEquals(intCode));
assertThrows(NullPointerException.class, () -> WithLongCode.INSTANCE.isCodeEquals(longCode));
}
private enum WithCode implements IWithCode<String> {
INSTANCE("testCode"),
SAME_CODE_INSTANCE("testCode"),
WRONG_CODE_INSTANCE("wrongCode"),
;
@Nonnull
private final String code;
WithCode(String code) {
checkNotNull(code);
this.code = code;
}
@Override
@Nonnull
public String getCode() {
return code;
}
}
private enum WithIntCode implements IWithIntCode {
INSTANCE(0),
SAME_CODE_INSTANCE(0),
WRONG_CODE_INSTANCE(1),
;
private final int code;
WithIntCode(int code) {
this.code = code;
}
@Override
public int getCode() {
return code;
}
}
private enum WithLongCode implements IWithLongCode {
INSTANCE(0L),
SAME_CODE_INSTANCE(0L),
WRONG_CODE_INSTANCE(108L),
;
private final long code;
WithLongCode(long code) {
this.code = code;
}
@Override
public long getCode() {
return code;
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.collection;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.collect.TreeRangeSet;
public class CollectionToolsTests {
@Test
void testIsEmpty() {
// Collection
List<String> list = new ArrayList<>();
assertTrue(CollectionTools.isEmpty(list));
assertFalse(CollectionTools.isNotEmpty(list));
list.add("Test");
assertFalse(CollectionTools.isEmpty(list));
assertTrue(CollectionTools.isNotEmpty(list));
// Map
Map<String, Integer> map = new HashMap<>();
assertTrue(CollectionTools.isEmpty(map));
assertFalse(CollectionTools.isNotEmpty(map));
map.put("2", 2);
assertFalse(CollectionTools.isEmpty(map));
assertTrue(CollectionTools.isNotEmpty(map));
// Table
Table<String, String, Integer> table = HashBasedTable.create();
assertTrue(CollectionTools.isEmpty(table));
assertFalse(CollectionTools.isNotEmpty(table));
table.put("ABC", "d", 4);
assertFalse(CollectionTools.isEmpty(table));
assertTrue(CollectionTools.isNotEmpty(table));
// Multimap
Multimap<String, String> multimap = HashMultimap.create();
assertTrue(CollectionTools.isEmpty(multimap));
assertFalse(CollectionTools.isNotEmpty(multimap));
multimap.put("ABC", "d");
assertFalse(CollectionTools.isEmpty(multimap));
assertTrue(CollectionTools.isNotEmpty(multimap));
// Multiset
Multiset<String> multiset = HashMultiset.create();
assertTrue(CollectionTools.isEmpty(multiset));
assertFalse(CollectionTools.isNotEmpty(multiset));
multiset.add("ABC");
assertFalse(CollectionTools.isEmpty(multiset));
assertTrue(CollectionTools.isNotEmpty(multiset));
// RangeSet
RangeSet<Integer> rangeSet = TreeRangeSet.create();
assertTrue(CollectionTools.isEmpty(rangeSet));
assertFalse(CollectionTools.isNotEmpty(rangeSet));
rangeSet.add(Range.closed(0, 100));
rangeSet.add(Range.openClosed(100, 200));
assertFalse(CollectionTools.isEmpty(rangeSet));
assertTrue(CollectionTools.isNotEmpty(rangeSet));
}
@Test
void testNullToEmpty() {
List<String> list = Lists.newArrayList("Java", "C", "C++", "C#");
assertSame(list, CollectionTools.nullToEmptyList(list));
assertEquals(Collections.emptyList(), CollectionTools.nullToEmptyList(null));
Set<String> set = Sets.newHashSet("Java", "C", "C++", "C#");
assertSame(set, CollectionTools.nullToEmptySet(set));
assertEquals(Collections.emptySet(), CollectionTools.nullToEmptySet(null));
Map<String, Integer> map = ImmutableMap.of("K1", 1, "K2", 2, "K3", 3);
assertSame(map, CollectionTools.nullToEmptyMap(map));
assertEquals(Collections.emptyMap(), CollectionTools.nullToEmptyMap(null));
}
@Test
void test_constructor_isNotAccessible_ThrowsIllegalStateException() {
Constructor<?>[] constructors = CollectionTools.class.getDeclaredConstructors();
Arrays.stream(constructors)
.forEach(constructor -> {
assertFalse(constructor.isAccessible());
constructor.setAccessible(true);
Throwable cause = assertThrows(Exception.class, constructor::newInstance)
.getCause();
assertInstanceOf(IllegalStateException.class, cause);
assertEquals("Utility class", cause.getMessage());
});
}
}

View File

@@ -0,0 +1,290 @@
/*
* Copyright 2025-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.collection;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nullable;
import org.junit.jupiter.api.Test;
import com.google.common.collect.ImmutableMap;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MapModifierTests {
private static final String APP_START_ID = UUID.randomUUID().toString();
private static final String LOCKED = "LOCKED";
private static final Map<String, String> commonProperties = ImmutableMap.<String, String>builder()
.put("channel", "MOBILE")
.put("appStartId", APP_START_ID)
.build();
@Test
void demo() {
Map<String, String> expected = new HashMap<String, String>() {
{
put("channel", "MOBILE");
put("appStartId", APP_START_ID);
put("username", "Ben");
put("accountStatus", LOCKED);
}
};
// MapModifier
MapModifier<String, String> modifier = new MapModifier<String, String>()
.putAll(commonProperties)
.put("username", "Ben")
.put("accountStatus", LOCKED);
// 从 Supplier 中获取 Map并修改数据
HashMap<String, String> hashMap1 = modifier.getAndModify(HashMap::new);
assertEquals(expected, hashMap1);
// 可以灵活使用不同 Map 类型的不同构造器
HashMap<String, String> hashMap2 = modifier.getAndModify(() -> new HashMap<>(8));
assertEquals(expected, hashMap2);
// HashMap<String, String> hashMap3 = modifier.getAndModify(() -> new HashMap<>(anotherMap));
TreeMap<String, String> treeMap = modifier.getAndModify(TreeMap::new);
assertEquals(expected, treeMap);
ConcurrentHashMap<String, String> concurrentHashMap = modifier.getAndModify(ConcurrentHashMap::new);
assertEquals(expected, concurrentHashMap);
assertNull(modifier.getAndModify(() -> (Map<String, String>) null));
// 修改已有的 Map
Map<String, String> srcMap = new HashMap<>();
srcMap.put("srcKey1", "srcValue1");
srcMap.put("srcKey2", "srcValue2");
modifier.modify(srcMap);
assertEquals(new HashMap<String, String>() {
{
putAll(commonProperties);
put("username", "Ben");
put("accountStatus", LOCKED);
put("srcKey1", "srcValue1");
put("srcKey2", "srcValue2");
}
}, srcMap);
assertDoesNotThrow(() -> modifier.modify((Map<String, String>) null));
// 创建一个有初始化数据的不可变的 Map
Map<String, String> unmodifiableMap = modifier.getUnmodifiableMap();
assertEquals(expected, unmodifiableMap);
assertThrows(UnsupportedOperationException.class,
() -> unmodifiableMap.put("key", "value"));
}
@Test
void createAndInitData() {
// 链式调用创建并初始化数据
HashMap<String, String> map = new MapModifier<String, String>()
.putAll(commonProperties)
.put("username", "Ben")
.put("accountStatus", LOCKED)
.getAndModify(HashMap::new);
HashMap<String, String> expected = new HashMap<String, String>() {
{
put("channel", "MOBILE");
put("appStartId", APP_START_ID);
put("username", "Ben");
put("accountStatus", LOCKED);
}
};
assertEquals(expected, map);
}
@Test
void put() {
Map<String, String> map = new MapModifier<String, String>()
.put("key1", "value0")
.put("key1", "value1")
.getAndModify(HashMap::new);
assertEquals(new HashMap<String, String>() {
{
put("key1", "value0");
put("key1", "value1");
}
}, map);
new MapModifier<String, String>()
.put("key1", "newValue1")
.put("key2", null)
.modify(map);
assertEquals("newValue1", map.get("key1"));
assertTrue(map.containsKey("key2"));
assertNull(map.get("key2"));
}
@Test
void putIfAbsent() {
Map<String, String> map = new MapModifier<String, String>()
.putIfAbsent("key1", null)
.putIfAbsent("key1", "value1")
.putIfAbsent("key1", "value2")
.getAndModify(HashMap::new);
assertEquals(new HashMap<String, String>() {
{
putIfAbsent("key1", null);
putIfAbsent("key1", "value1");
putIfAbsent("key1", "value2");
}
}, map);
new MapModifier<String, String>()
.putIfAbsent("key1", "newValue1")
.modify(map);
assertTrue(map.containsKey("key1"));
assertEquals("value1", map.get("key1"));
}
@Test
void putAll_map() {
Map<String, String> entries = new HashMap<String, String>() {
{
put("key1", "value1");
put("key2", "value2");
}
};
Map<String, String> map = new MapModifier<String, String>()
.putAll((Map<String, String>) null)
.putAll(Collections.emptyMap())
.putAll(entries)
.getAndModify(HashMap::new);
assertEquals(entries, map);
new MapModifier<String, String>()
.putAll(new HashMap<String, String>() {
{
put("key2", "newValue2");
put("key3", "value3");
}
})
.modify(map);
assertEquals(new HashMap<String, String>() {
{
put("key1", "value1");
put("key2", "value2");
put("key2", "newValue2");
put("key3", "value3");
}
}, map);
}
@Test
void computeIfAbsent_keyAndFunction() {
Map<String, String> map = new MapModifier<String, String>()
.computeIfAbsent("key1", k -> null)
.computeIfAbsent("key1", k -> "value1")
.computeIfAbsent("key1", k -> "value2")
.getAndModify(HashMap::new);
assertEquals(new HashMap<String, String>() {
{
computeIfAbsent("key1", k -> null);
computeIfAbsent("key1", k -> "value1");
computeIfAbsent("key1", k -> "value2");
}
}, map);
new MapModifier<String, String>()
.computeIfAbsent("key1", k -> "newValue1")
.modify(map);
assertNotNull(map);
assertTrue(map.containsKey("key1"));
assertEquals("value1", map.get("key1"));
}
@Test
void computeIfPresent_keyAndBiFunction() {
Map<String, String> map = new HashMap<String, String>() {{
put("key1", "value1");
}};
new MapModifier<String, String>()
.computeIfPresent("key1", (k, v) -> k + v)
.computeIfPresent("key2", (k, v) -> k + v)
.modify(map);
assertEquals(new HashMap<String, String>() {{
put("key1", "key1value1");
}}, map);
}
@Test
void remove() {
Map<String, String> map = new HashMap<String, String>() {{
put("key1", "value1");
put("key2", "value2");
}};
new MapModifier<String, String>()
.remove("key2")
.modify(map);
assertEquals(new HashMap<String, String>() {{
put("key1", "value1");
}}, map);
}
@Test
void clear() {
Map<String, String> map = new HashMap<String, String>() {{
put("key1", "value1");
put("key2", "value2");
}};
new MapModifier<String, String>()
.clear()
.modify(map);
assertTrue(map.isEmpty());
}
@Getter
static class SimpleEntry<K, V> implements Map.Entry<K, V> {
private final K key;
private final V value;
public SimpleEntry(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public V setValue(@Nullable V value) {
throw new UnsupportedOperationException("Unimplemented method 'setValue'");
}
}
}

View File

@@ -0,0 +1,271 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.constant;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.regex.Matcher;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public //
class PatternConstsTests {
// ================================
// #region - BASIC_ISO_DATE
// ================================
@Test
void testBasicIsoDate_ValidDate() {
Matcher matcher = PatternConsts.BASIC_ISO_DATE.matcher("20241229");
assertTrue(matcher.matches());
assertEquals("2024", matcher.group(1));
assertEquals("12", matcher.group(2));
assertEquals("29", matcher.group(3));
assertEquals("2024", matcher.group("yyyy"));
assertEquals("12", matcher.group("MM"));
assertEquals("29", matcher.group("dd"));
// LeapYearFeb29()
assertTrue(PatternConsts.BASIC_ISO_DATE.matcher("20200229").matches());
// BoundaryMin()
assertTrue(PatternConsts.BASIC_ISO_DATE.matcher("00000101").matches());
// BoundaryMax()
assertTrue(PatternConsts.BASIC_ISO_DATE.matcher("9999999991231").matches());
}
@ParameterizedTest
@ValueSource(strings = {
"20231301", // InvalidMonth
"20230230", // InvalidDay
"20210229", // NonLeapYearFeb29
})
void testBasicIsoDate_InvalidDate_butMatches(String date) {
// 虽然日期有误,但这个正则无法判断。实际工作中,应使用日期时间 API。
Matcher matcher = PatternConsts.BASIC_ISO_DATE.matcher(date);
assertTrue(matcher.matches());
}
@ParameterizedTest
@ValueSource(strings = {
"2023041", // TooShort
"99999999990415", // TooLong
"2023-04-15", // NonNumeric
})
void testBasicIsoDate_InvalidDate_Mismatches(String date) {
Matcher matcher = PatternConsts.BASIC_ISO_DATE.matcher(date);
assertFalse(matcher.matches());
}
// ================================
// #endregion - BASIC_ISO_DATE
// ================================
// ================================
// #region - ISO_LOCAL_DATE
// ================================
@Test
void testIsoLocalDate_ValidDate() {
Matcher matcher = PatternConsts.ISO_LOCAL_DATE.matcher("2024-12-29");
assertTrue(matcher.matches());
assertEquals("2024", matcher.group("yyyy"));
assertEquals("12", matcher.group("MM"));
assertEquals("29", matcher.group("dd"));
// LeapYearFeb29()
assertTrue(PatternConsts.ISO_LOCAL_DATE.matcher("2020-02-29").matches());
// BoundaryMin()
assertTrue(PatternConsts.ISO_LOCAL_DATE.matcher("0000-01-01").matches());
// BoundaryMax()
assertTrue(PatternConsts.ISO_LOCAL_DATE.matcher("999999999-12-31").matches());
}
@ParameterizedTest
@ValueSource(strings = {
"2023-13-01", // InvalidMonth
"2023-02-30", // InvalidDay
"2021-02-29", // NonLeapYearFeb29
})
void testIsoLocalDate_InvalidDate_butMatches(String date) {
// 虽然日期有误,但这个正则无法判断。实际工作中,应使用日期时间 API。
Matcher matcher = PatternConsts.ISO_LOCAL_DATE.matcher(date);
assertTrue(matcher.matches());
}
@ParameterizedTest
@ValueSource(strings = {
"2023-04-1", // TooShort
"9999999999-04-15", // TooLong
"20230415",
})
void testIsoLocalDate_InvalidDate_Mismatches(String date) {
Matcher matcher = PatternConsts.ISO_LOCAL_DATE.matcher(date);
assertFalse(matcher.matches());
}
// ================================
// #endregion - ISO_LOCAL_DATE
// ================================
// ================================
// #region - PASSWORD
// ================================
@Test
void testPassword_ValidPassword_Matches() {
assertTrue(PatternConsts.PASSWORD.matcher("Abc123!@#").matches());
}
@Test
void testPassword_InvalidPassword_Mismatches() {
assertFalse(PatternConsts.PASSWORD.matcher("Abc123 !@#").matches()); // 带空格
assertFalse(PatternConsts.PASSWORD.matcher("Abc123!@# ").matches()); // 带空格
assertFalse(PatternConsts.PASSWORD.matcher(" Abc123!@#").matches()); // 带空格
assertFalse(PatternConsts.PASSWORD.matcher(" Abc123!@# ").matches()); // 带空格
assertFalse(PatternConsts.PASSWORD.matcher("77553366998844113322").matches()); // 纯数字
assertFalse(PatternConsts.PASSWORD.matcher("poiujhgbfdsazxcfvghj").matches()); // 纯小写字母
assertFalse(PatternConsts.PASSWORD.matcher("POIUJHGBFDSAZXCFVGHJ").matches()); // 纯大写字母
assertFalse(PatternConsts.PASSWORD.matcher("!#$%&'*\\+-/=?^`{|}~@()[]\",.;':").matches()); // 纯特殊字符
assertFalse(PatternConsts.PASSWORD.matcher("sdfrghbv525842582752").matches()); // 没有小写字母
assertFalse(PatternConsts.PASSWORD.matcher("SDFRGHBV525842582752").matches()); // 没有小写字母
assertFalse(PatternConsts.PASSWORD.matcher("sdfrghbvSDFRGHBV").matches()); // 没有数字
assertFalse(PatternConsts.PASSWORD.matcher("Abc1!").matches()); // 太短
assertFalse(PatternConsts.PASSWORD.matcher("Abc1!Abc1!Abc1!Abc1!Abc1!Abc1!Abc1!").matches()); // 太长
assertFalse(PatternConsts.PASSWORD.matcher("").matches());
assertFalse(PatternConsts.PASSWORD.matcher(" ").matches());
}
// ================================
// #endregion - PASSWORD
// ================================
// ================================
// #region - EMAIL
// ================================
@Test
public void testValidEmails() {
assertTrue(PatternConsts.EMAIL.matcher("test@example.com").matches());
assertTrue(PatternConsts.EMAIL.matcher("user.name+tag+sorting@example.com").matches());
assertTrue(PatternConsts.EMAIL.matcher("user@sub.example.com").matches());
assertTrue(PatternConsts.EMAIL.matcher("user@123.123.123.123").matches());
}
@Test
public void testInvalidEmails() {
assertFalse(PatternConsts.EMAIL.matcher(".username@example.com").matches());
assertFalse(PatternConsts.EMAIL.matcher("@missingusername.com").matches());
assertFalse(PatternConsts.EMAIL.matcher("plainaddress").matches());
assertFalse(PatternConsts.EMAIL.matcher("username..username@example.com").matches());
assertFalse(PatternConsts.EMAIL.matcher("username.@example.com").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@-example.com").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@-example.com").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@.com.com").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@.com.my").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@.com").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@com.").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@com").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@example..com").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@example.com-").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@example.com.").matches());
assertFalse(PatternConsts.EMAIL.matcher("username@example").matches());
}
// ================================
// #endregion - EMAIL
// ================================
// ================================
// #region - Chinese2ndIdCardNumber
// ================================
@ParameterizedTest
@ValueSource(strings = {
"44520019900101456X",
"44520019900101456x",
"445200199001014566",
})
void testChinese2ndIdCardNumber_ValidChinese2ndIdCardNumber(String value) {
Matcher matcher = PatternConsts.CHINESE_2ND_ID_CARD_NUMBER.matcher(value);
assertTrue(matcher.matches());
assertEquals("44", matcher.group("province"));
assertEquals("4452", matcher.group("city"));
assertEquals("445200", matcher.group("county"));
}
@ParameterizedTest
@ValueSource(strings = {
"4452200199001014566",
"44520199001014566",
" ",
"",
})
void testChinese2ndIdCardNumber_InvalidChinese2ndIdCardNumber(String value) {
assertFalse(PatternConsts.CHINESE_2ND_ID_CARD_NUMBER.matcher(value).matches());
}
// ================================
// #endregion - Chinese2ndIdCardNumber
// ================================
// ================================
// #region - invoke constructor
// ================================
@Test
void test_constructor_isNotAccessible_ThrowsIllegalStateException() {
Constructor<?>[] constructors;
constructors = RegexConsts.class.getDeclaredConstructors();
Arrays.stream(constructors)
.forEach(constructor -> {
assertFalse(constructor.isAccessible());
constructor.setAccessible(true);
Throwable cause = assertThrows(Exception.class, constructor::newInstance)
.getCause();
assertInstanceOf(IllegalStateException.class, cause);
assertEquals("Utility class", cause.getMessage());
});
constructors = PatternConsts.class.getDeclaredConstructors();
Arrays.stream(constructors)
.forEach(constructor -> {
assertFalse(constructor.isAccessible());
constructor.setAccessible(true);
Throwable cause = assertThrows(Exception.class, constructor::newInstance)
.getCause();
assertInstanceOf(IllegalStateException.class, cause);
assertEquals("Utility class", cause.getMessage());
});
}
// ================================
// #endregion - invoke constructor
// ================================
}

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