55 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
125 changed files with 4014 additions and 1392 deletions

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.

235
README.md
View File

@@ -1,221 +1,38 @@
## 一、annotation - 注解
### 1. StaticFactoryMethod
标识静态工厂方法。 *《Effective Java》***Item1** 建议考虑用静态工厂方法替换构造器, 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分
# Plusone Commons
## 1. 简介
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发
### 2. ReaderMethod 和 WriterMethod
分别标识读方法(如 getter或写方法如 setter
一开始是为了补充日常开发中guava 认为不需要,而我又用得上的工具,所以需要结合 guava 使用。后面也包含了一些从日常工作与学习中抽离出来可以通用的东西。
最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁
Plusone Commons 的工具类不追求“大而全”,而是只提供相对需要的部分功能
### 3. UnsupportedOperation
标识该方法不被支持或没有实现,将抛出 `UnsupportedOperationException`。 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。
> 未来一些不够“通用”的组件会迁移到更合适的模块中。
### 4. Virtual
Java 非 final 的实例方法,对应 C++/C# 中的虚方法,允许被子类覆写。 Virtual 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写
## 2. 安装
项目基于 OpenJDK 8 和 maven 构建
### 5. ValueObject
标记一个类,表示其作为值对象,区别于 Entity。
## 二、base - 基础组件
### 1. Ref
`Ref` 包装了一个值,表示对该值的应用。
灵感来自于 C# 的 ref 参数修饰符。C# 允许通过以下方式,将值返回给调用端:
```C#
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
```
### 2. IWithCode
类似于枚举这样的类型,通常需要设置固定的码值表示对应的含义。 可实现 `IWithCode`、`IWithIntCode`、`IWithLongCode`,便于在需要的地方对这些接口的实现进行处理。
## 三、collection - 集合
### 1. CollectionTools
集合工具类
## 四、constant - 常量
### 1. 正则常量
`RegexConsts` 包含常见正则表达式;`PatternConsts` 包含对应的 `Pattern` 对象
## 五、exception - 异常
### 1. MultiTypesException - 多类型异常
异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。
异常实现 `MultiTypesException` 的 `MultiTypesException#getType` 方法,返回对应的场景类型。
表示场景类型的枚举实现 `MultiTypesException.ExceptionType`,其中的工厂方法用于创建对应类型的异常。
```java
public final class LoginException
extends RuntimeException
implements MultiTypesException<LoginException, 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 ExceptionType {
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(String message) {
return new LoginException(this, message);
}
@Override
public @Nonnull LoginException create(Throwable cause) {
return new LoginException(this, cause);
}
@Override
public @Nonnull LoginException create(String message, Throwable cause) {
return new LoginException(this, message, cause);
}
}
}
```
使用时,可以使用这种方式创建并抛出异常:
```java
throw LoginException.Type.TOKEN_TIMEOUT.create();
使用 Maven 添加依赖:
```xml
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-commons</artifactId>
<version>${plusone-commons.version}</version>
</dependency>
```
### 2. 业务异常
预设常见的业务异常。可继承 `BizException` 自定义业务异常。
## 3. 功能
详细功能说明请查阅文档:
### 3. 系统异常
预设常见的系统异常。可继承 `SysException` 自定义系统异常。
+ [文档地址](/plusone-commons/docs)
## 六、function - 函数式编程
### 1. PredicateTools
`PredicateTools` 用于 `Predicate` 的相关操作。
## 4. 代码仓库
项目仓库一共建了三个:
### 2. Functional interfaces
补充可能用得上的函数式接口:
+ [GitHub](https://github.com/ZhouXY108/plusone-commons)
+ [gitee](https://gitee.com/zhouxy108/plusone-commons)
+ [自建 Gitea 仓库](https://gitea.zhouxy.xyz/plusone/plusone-commons)
| 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) |
欢迎在 GitHub 和 gitee 上通过 issue 反馈使用过程中发现的问题和建议,也接受善意的 PR。
## 七、model - 业务建模组件
包含业务建模可能用到的性别、身份证等元素,也包含数据传输对象,如分页查询参数、响应结果、分页结果等。
### 数据传输对象
#### 1. 分页
分页组件由 `PagingAndSortingQueryParams` 作为入参, 因为分页必须伴随着排序,不然可能出现同一个对象重复出现在不同页,有的对象不被查询到的情况, 所以分页查询的入参必须包含排序条件。
用户可继承 `PagingAndSortingQueryParams` 构建自己的分页查询入参,需在构造器中调用 `PagingAndSortingQueryParams` 的构造器,传入一个 Map 作为白名单, key 是供前端指定用于排序的**属性名**value 是对应数据库中的**字段名**,只有在白名单中指定的属性名才允许作为排序条件。
`PagingAndSortingQueryParams` 包含三个主要的属性:
- **size** - 每页显示的记录数
- **pageNum** - 当前页码
- **orderBy** - 排序条件
其中 `orderBy` 是一个 List可以指定多个排序条件每个排序条件是一个字符串 格式为“**属性名-ASC**”或“**属性名-DESC**”,分别表示升序和降序。
比如前端传入的 orderBy 为 ["name-ASC","age-DESC"],意味着要按 name 进行升序name 相同的情况下则按 age 进行降序。
使用时调用 `PagingAndSortingQueryParams#buildPagingParams()` 方法获取分页参数 `PagingParams`。
分页结果可以存放到 `PageResult` 中,作为出参。
#### 2. UnifiedResponse
UnifiedResponse 对返回给前端的数据进行封装,包含 `code`、`message`、`data。`
可使用 `UnifiedResponses` 快速构建 `UnifiedResponse` 对象。 `UnifiedResponses` 默认的成功代码为 "2000000" 用户按测试类 `CustomUnifiedResponseFactoryTests` 中所示范的,继承 `UnifiedResponses` 实现自己的工厂类, 自定义 `SUCCESS_CODE` 和 `DEFAULT_SUCCESS_MSG` 和工厂方法。 见 [issue#22](http://zhouxy.xyz:3000/plusone/plusone-commons/issues/22)。
## 八、time - 时间 API
### 1. 季度
模仿 JDK 的 `java.time.Month` 和 `java.time.YearMonth` 实现 `Quarter`、`YearQuarter`,对季度进行建模。
## 九、util - 工具类
包含树构建器(`TreeBuilder`)、断言工具(`AssertTools`、ID 生成器(`IdGenerator`)及其它实用工具类。
## 5. 许可
项目使用 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) 开源,相关声明请参阅 `NOTICE` 文件。

View File

@@ -5,11 +5,11 @@
],
"dictionaryDefinitions": [],
"dictionaries": [],
"words": [],
"ignoreWords": [
"words": [
"aliyun",
"baomidou",
"Batis",
"buildmetadata",
"Consolas",
"cspell",
"databind",
@@ -33,6 +33,7 @@
"Nonnull",
"NOSONAR",
"okhttp",
"okio",
"ooxml",
"overriden",
"plusone",

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` 对象

View File

@@ -7,21 +7,16 @@
<parent>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-parent</artifactId>
<version>1.1.0-RC1</version>
<version>1.1.0-SNAPSHOT</version>
</parent>
<artifactId>plusone-commons</artifactId>
<description>
常见工具集,结合 guava 使用。
</description>
<name>Plusone Commons</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<description>
Plusone Commons 是一个 Java 工具类库,提供了一系列实用的类和方法,用于简化开发。结合 guava 使用。
</description>
<dependencyManagement>
<dependencies>
@@ -121,4 +116,20 @@
</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 2023-2025 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.
@@ -27,7 +27,7 @@ import java.lang.annotation.Target;
* <p>
* 标识方法是读方法,如 getter。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see WriterMethod
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +26,7 @@ import java.lang.annotation.Target;
*
* <p>标识方法为静态工厂方法
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
@Target(ElementType.METHOD)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.
@@ -27,7 +27,7 @@ import java.lang.annotation.Documented;
*
* <p>标识方法为不支持的操作。该方法将抛出 {@link UnsupportedOperationException}。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @version 1.0
* @since 1.0.0
* @see UnsupportedOperationException

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +25,7 @@ import java.lang.annotation.Target;
/**
* ValueObject - 值对象
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
@Inherited

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +25,7 @@ import java.lang.annotation.Target;
* 标识该方法是虚方法。
* <p>该注解用于提醒、强调父类虽然有默认实现,但子类可以根据自己的需要覆写。</p>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
@Target(ElementType.METHOD)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.
@@ -27,7 +27,7 @@ import java.lang.annotation.Target;
* <p>
* 标识方法是写方法,如 setter。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see ReaderMethod
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -15,45 +15,8 @@
*/
/**
* <h2>注解</h2>
* 注解
*
* <h3>
* 1. {@link StaticFactoryMethod}
* </h3>
* <p>
* 标识<b>静态工厂方法</b>。
* 《Effective Java》的 Item1 建议考虑用静态工厂方法替换构造器,
* 因而考虑有一个注解可以标记一下静态工厂方法,以和其它方法进行区分。
*
* <h3>
* 2. {@link ReaderMethod} 和 {@link WriterMethod}
* </h3>
* <p>
* 分别标识<b>读方法</b>(如 getter或<b>写方法</b>(如 setter
*
* <p>
* 最早是写了一个集合类,为了方便判断使用读写锁时,哪些情况下使用读锁,哪些情况下使用写锁。
*
* <h3>
* 3. {@link UnsupportedOperation}
* </h3>
* <p>
* 标识该方法不被支持或没有实现,将抛出 {@link UnsupportedOperationException}。
* 为了方便在使用时,不需要点进源码,就能知道该方法没有实现。
*
* <h3>
* 4. {@link Virtual}
* </h3>
* <p>
* Java 非 final 的实例方法,对应 C++/C&num; 中的虚方法,允许被子类覆写。
* {@link Virtual} 注解旨在设计父类时,强调该方法父类虽然有默认实现,但子类可以根据自己的需要覆写。
*
* <h3>
* 5. {@link ValueObject}
* </h3>
* <p>
* 标记一个类,表示其作为值对象,区别于 Entity。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.annotation;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2025 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.
@@ -26,7 +26,7 @@ import javax.annotation.Nullable;
* 用于像自定义异常等需要带有 {@code code} 字段的类,
* 方便其它地方的程序判断该类的是否实现了此接口,以此获取其实例的 {@code code} 字段的值。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public interface IWithCode<T> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2025 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.
@@ -25,7 +25,7 @@ import javax.annotation.Nullable;
* 用于像自定义异常等需要带有 {@code code} 字段的类,
* 方便其它地方的程序判断该类的是否实现了此接口,以此获取其实例的 {@code code} 字段的值。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public interface IWithIntCode {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2025 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.
@@ -25,7 +25,7 @@ import javax.annotation.Nullable;
* 用于像自定义异常等需要带有 {@code code} 字段的类,
* 方便其它地方的程序判断该类的是否实现了此接口,以此获取其实例的 {@code code} 字段的值。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public interface IWithLongCode {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -15,62 +15,14 @@
*/
/**
* <h2>基础组件</h2>
* 基础内容
*
* <h3>1. Ref</h3>
* <p>
* {@link Ref} 包装了一个值,表示对该值的应用。
*
* <p>灵感来自于 C&num; 的 {@code ref} 参数修饰符。C&num; 允许通过以下方式,将值返回给调用端:</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(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(Ref&lt;Integer&gt; intRefArgument, 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>
*
* <h3>2. IWithCode</h3>
* <p>
* 类似于枚举这样的类型,通常需要设置固定的码值表示对应的含义。
* 可实现 {@link IWithCode}、{@link IWithIntCode}、{@link IWithLongCode},便于在需要的地方对这些接口的实现进行处理。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
@CheckReturnValue
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.base;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.CheckReturnValue;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.
@@ -33,7 +33,7 @@ import com.google.common.collect.Table;
/**
* 集合工具类
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public class CollectionTools {

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 2025 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.
@@ -15,14 +15,9 @@
*/
/**
* <h2>集合</h2>
* 集合相关工具
*
* <h3>
* 1. {@link CollectionTools}
* </h3>
* 集合工具类
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.collection;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +21,7 @@ import java.util.regex.Pattern;
/**
* 正则表达式常量
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @see RegexConsts
* @see xyz.zhouxy.plusone.commons.util.RegexTools
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2025 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.
@@ -19,7 +19,7 @@ package xyz.zhouxy.plusone.commons.constant;
/**
* 正则表达式常量
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @see PatternConsts
*/
public final class RegexConsts {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -22,7 +22,7 @@
* </h3>
* {@link RegexConsts} 包含常见正则表达式;{@link PatternConsts} 包含对应的 {@link Pattern} 对象。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.constant;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -19,7 +19,7 @@ package xyz.zhouxy.plusone.commons.exception;
/**
* 数据不存在异常
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public final class DataNotExistsException extends Exception {

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

@@ -0,0 +1,41 @@
/*
* 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 xyz.zhouxy.plusone.commons.annotation.Virtual;
import xyz.zhouxy.plusone.commons.base.IWithCode;
/**
* 异常场景
*
* @param <TCode> 场景编码
* @author ZhouXY
*/
public interface IExceptionType<TCode> extends IWithCode<TCode> {
/**
* 默认异常信息
*
* @return 默认异常信息
*/
String getDefaultMessage();
@Virtual
default String getDescription() {
return getDefaultMessage();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -17,24 +17,23 @@ package xyz.zhouxy.plusone.commons.exception;
import javax.annotation.Nonnull;
import xyz.zhouxy.plusone.commons.base.IWithCode;
/**
* MultiTypesException
* IMultiTypesException
*
* <p>
* 异常在不同场景下被抛出可以用不同的枚举值表示不同的场景类型
*
* <p>
* 异常实现 {@link MultiTypesException} {@link #getType} 方法返回对应的场景类型
* 异常实现 {@link IMultiTypesException} {@link #getType} 方法返回对应的场景类型
*
* <p>
* 表示场景类型的枚举实现 {@link ExceptionType}其中的工厂方法用于创建对应类型的异常
* 表示场景类型的枚举实现 {@link IExceptionType}各个枚举值本身就是该场景的异常的工厂实例
* 使用其中的工厂方法用于创建对应类型的异常
*
* <pre>
* public final class LoginException
* extends RuntimeException
* implements MultiTypesException&lt;LoginException, LoginException.Type&gt; {
* 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) {
@@ -61,7 +60,7 @@ import xyz.zhouxy.plusone.commons.base.IWithCode;
*
* // ...
*
* public enum Type implements ExceptionType&lt;LoginException&gt; {
* public enum Type implements IExceptionType&lt;String&gt;, IExceptionFactory&lt;LoginException&gt; {
* DEFAULT("00", "当前会话未登录"),
* NOT_TOKEN("10", "未提供token"),
* INVALID_TOKEN("20", "token无效"),
@@ -118,73 +117,17 @@ import xyz.zhouxy.plusone.commons.base.IWithCode;
* throw LoginException.Type.TOKEN_TIMEOUT.create();
* </pre>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @param <T> 异常场景
* @author ZhouXY
* @since 1.0.0
*/
public interface MultiTypesException<E extends Exception, T extends MultiTypesException.ExceptionType<E>> {
public interface IMultiTypesException<T extends IExceptionType<?>> {
/**
* 异常类型
*
* @return 异常类型通常是实现了 {@link ExceptionType} 的枚举
* @return 异常类型通常是实现了 {@link IExceptionType} 的枚举
*/
@Nonnull
T getType();
/**
* 获取异常类型编码
*
* @return 异常类型编码
*/
default @Nonnull String getTypeCode() {
return getType().getCode();
}
/**
* 异常类型
*/
public static interface ExceptionType<E extends Exception> extends IWithCode<String> {
/**
* 默认异常信息
*/
String getDefaultMessage();
/**
* 创建异常
*
* @return 异常对象
*/
@Nonnull
E create();
/**
* 使用指定 {@code message} 创建异常
*
* @param message 异常信息
* @return 异常对象
*/
@Nonnull
E create(String message);
/**
* 使用指定 {@code cause} 创建异常
*
* @param cause 包装的异常
* @return 异常对象
*/
@Nonnull
E create(Throwable cause);
/**
* 使用指定 {@code message} {@code cause} 创建异常
*
* @param message 异常信息
* @param cause 包装的异常
* @return 异常对象
*/
@Nonnull
E create(String message, Throwable cause);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -21,7 +21,6 @@ import java.time.format.DateTimeParseException;
import javax.annotation.Nonnull;
import xyz.zhouxy.plusone.commons.exception.business.RequestParamsException;
import xyz.zhouxy.plusone.commons.exception.MultiTypesException.ExceptionType;
/**
* 解析失败异常
@@ -34,12 +33,12 @@ import xyz.zhouxy.plusone.commons.exception.MultiTypesException.ExceptionType;
* throw new RequestParamsException(ParsingFailureException.Type.NUMBER_PARSING_FAILURE.create());
* </pre>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public final class ParsingFailureException
extends Exception
implements MultiTypesException<ParsingFailureException, ParsingFailureException.Type> {
implements IMultiTypesException<ParsingFailureException.Type> {
private static final long serialVersionUID = 795996090625132616L;
private final Type type;
@@ -171,7 +170,7 @@ public final class ParsingFailureException
/** XML 解析失败 */
public static final Type XML_PARSING_FAILURE = Type.XML_PARSING_FAILURE;
public enum Type implements ExceptionType<ParsingFailureException> {
public enum Type implements IExceptionType<String>, IExceptionFactory<ParsingFailureException> {
DEFAULT("00", "解析失败"),
NUMBER_PARSING_FAILURE("10", "数字转换失败"),
DATE_TIME_PARSING_FAILURE("20", "时间解析失败"),
@@ -184,7 +183,7 @@ public final class ParsingFailureException
@Nonnull
private final String defaultMessage;
Type(String code, String defaultMessage) {
Type(@Nonnull String code, @Nonnull String defaultMessage) {
this.code = code;
this.defaultMessage = defaultMessage;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -25,7 +25,7 @@ package xyz.zhouxy.plusone.commons.exception.business;
* <p>
* <b>NOTE: 通常表示业务中的意外情况。如:用户错误输入、缺失必填字段、用户余额不足等。</b>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public class BizException extends RuntimeException {
@@ -33,21 +33,13 @@ public class BizException extends RuntimeException {
private static final String DEFAULT_MSG = "业务异常";
/**
* 使用默认 message 构造新的业务异常。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*/
public BizException() {
super(DEFAULT_MSG);
}
/**
* 使用指定的 {@code message} 构造新的业务异常。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*
* @param message 异常信息
*/
public BizException(String message) {
protected BizException(String message) {
super(message);
}
@@ -57,7 +49,7 @@ public class BizException extends RuntimeException {
*
* @param cause 包装的异常
*/
public BizException(Throwable cause) {
protected BizException(Throwable cause) {
super(cause);
}
@@ -67,8 +59,27 @@ public class BizException extends RuntimeException {
* @param message 异常信息
* @param cause 包装的异常
*/
public BizException(String message, Throwable 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

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -18,8 +18,9 @@ package xyz.zhouxy.plusone.commons.exception.business;
import javax.annotation.Nonnull;
import xyz.zhouxy.plusone.commons.exception.MultiTypesException.ExceptionType;
import xyz.zhouxy.plusone.commons.exception.MultiTypesException;
import xyz.zhouxy.plusone.commons.exception.IExceptionFactory;
import xyz.zhouxy.plusone.commons.exception.IExceptionType;
import xyz.zhouxy.plusone.commons.exception.IMultiTypesException;
/**
* InvalidInputException
@@ -30,12 +31,12 @@ import xyz.zhouxy.plusone.commons.exception.MultiTypesException;
* <p>
* <b>NOTE: 属业务异常</b>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public final class InvalidInputException
extends RequestParamsException
implements MultiTypesException<InvalidInputException, InvalidInputException.Type> {
implements IMultiTypesException<InvalidInputException.Type> {
private static final long serialVersionUID = -28994090625082516L;
private final Type type;
@@ -109,7 +110,7 @@ public final class InvalidInputException
return this.type;
}
public enum Type implements ExceptionType<InvalidInputException> {
public enum Type implements IExceptionType<String>, IExceptionFactory<InvalidInputException> {
DEFAULT("00", "用户输入内容非法"),
CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS("01", "包含非法恶意跳转链接"),
CONTAINS_ILLEGAL_WORDS("02", "包含违禁敏感词"),
@@ -122,7 +123,7 @@ public final class InvalidInputException
@Nonnull
private final String defaultMessage;
Type(String code, String defaultMsg) {
Type(@Nonnull String code, @Nonnull String defaultMsg) {
this.code = code;
this.defaultMessage = defaultMsg;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -22,7 +22,7 @@ package xyz.zhouxy.plusone.commons.exception.business;
* <p>
* 用户请求参数错误
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public class RequestParamsException extends BizException {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -17,6 +17,6 @@
/**
* 业务异常
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.exception.business;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -15,114 +15,8 @@
*/
/**
* <h2>异常</h2>
* 包含常见的业务异常与系统异常,以及异常相关的工具
*
* <h3>1. {@link MultiTypesException} - 多类型异常</h3>
* <p>
* 异常在不同场景下被抛出,可以用不同的枚举值,表示不同的场景类型。
*
* <p>
* 异常实现 {@link MultiTypesException} 的 {@link MultiTypesException#getType} 方法,返回对应的场景类型。
*
* <p>
* 表示场景类型的枚举实现 {@link MultiTypesException.ExceptionType},其中的工厂方法用于创建对应类型的异常。
*
* <pre>
* public final class LoginException
* extends RuntimeException
* implements MultiTypesException&lt;LoginException, 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 ExceptionType&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>
*
* <h3>2. 业务异常</h3>
* 预设常见的业务异常。可继承 {@link BizException} 自定义业务异常。
*
* <h3>3. 系统异常</h3>
* 预设常见的系统异常。可继承 {@link SysException} 自定义系统异常。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.exception;
import xyz.zhouxy.plusone.commons.exception.business.*;
import xyz.zhouxy.plusone.commons.exception.system.*;

View File

@@ -1,81 +0,0 @@
/*
* Copyright 2024-2025 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.exception.system;
/**
* DataOperationResultException
*
* <p>
* 当数据操作的结果不符合预期时抛出。
*
* <p>
* 比如当一个 insert 或 update 操作时,预计影响数据库中的一行数据,但结果却影响了零条数据或多条数据,
* 当出现这种始料未及的诡异情况时,抛出 {@link DataOperationResultException} 并回滚事务。
* 后续需要排查原因。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @since 1.0.0
*/
public final class DataOperationResultException extends SysException {
private static final long serialVersionUID = 992754090625352516L;
private final long expected;
private final long actual;
/**
* 创建一个 {@code DataOperationResultException} 对象
*
* @param expected 预期影响的行数
* @param actual 实际影响的行数
*/
public DataOperationResultException(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 DataOperationResultException} 对象
*
* @param expected 预期影响的行数
* @param actual 实际影响的行数
* @param message 错误信息
*/
public DataOperationResultException(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;
}
}

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

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +22,7 @@ package xyz.zhouxy.plusone.commons.exception.system;
* <p>
* 在无法找到可访问的 Mac 地址时抛出
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public class NoAvailableMacFoundException extends SysException {
@@ -33,7 +33,7 @@ public class NoAvailableMacFoundException extends SysException {
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*/
public NoAvailableMacFoundException() {
super();
super("无法找到可访问的 Mac 地址");
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -22,7 +22,7 @@ package xyz.zhouxy.plusone.commons.exception.system;
* <p>
* 通常表示应用代码存在问题,或因环境问题,引发异常。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public class SysException extends RuntimeException {
@@ -30,21 +30,13 @@ public class SysException extends RuntimeException {
private static final String DEFAULT_MSG = "系统异常";
/**
* 使用默认 message 构造新的系统异常。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*/
public SysException() {
super(DEFAULT_MSG);
}
/**
* 使用指定的 {@code message} 构造新的系统异常。
* {@code cause} 未初始化,后面可能会通过调用 {@link #initCause} 进行初始化。
*
* @param message 异常信息
*/
public SysException(String message) {
protected SysException(String message) {
super(message);
}
@@ -54,7 +46,7 @@ public class SysException extends RuntimeException {
*
* @param cause 包装的异常
*/
public SysException(Throwable cause) {
protected SysException(Throwable cause) {
super(cause);
}
@@ -64,7 +56,27 @@ public class SysException extends RuntimeException {
* @param message 异常信息
* @param cause 包装的异常
*/
public SysException(String message, Throwable 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

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -17,6 +17,6 @@
/**
* 系统异常
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.exception.system;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -25,7 +25,7 @@ import com.google.common.annotations.Beta;
* 一个特殊的 {@link java.util.function.UnaryOperator}。
* 表示对 {@code boolean} 值的一元操作。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.UnaryOperator
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -25,7 +25,7 @@ import com.google.common.annotations.Beta;
* 一个特殊的 {@link java.util.function.UnaryOperator}。
* 表示对 {@code char} 的一元操作。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.UnaryOperator
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -24,7 +24,7 @@ package xyz.zhouxy.plusone.commons.function;
*
* @param <E> 可抛出的异常类型
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
@FunctionalInterface

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +25,7 @@ import java.util.function.Supplier;
* <p>
* 返回 {@code Optional&lt;T&gt;} 对象。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see Optional
* @see Supplier

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +24,7 @@ import java.util.function.Predicate;
* <p>
* {@link Predicate} 相关操作。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see Predicate
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -22,7 +22,7 @@ package xyz.zhouxy.plusone.commons.function;
* <p>
* 允许抛出异常的消费操作。是一个特殊的 {@link java.util.function.Consumer}。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.Consumer
*/
@@ -33,6 +33,7 @@ public interface ThrowingConsumer<T, E extends Throwable> {
* 消费给定的参数,允许抛出异常
*
* @param t 要消费的参数
* @throws E 抛出的异常
*/
void accept(T t) throws E;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -25,7 +25,7 @@ package xyz.zhouxy.plusone.commons.function;
* @param <R> 返回结果类型
* @param <E> 异常类型
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0
* @see java.util.function.Function
*/
@@ -37,6 +37,7 @@ 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 2024-2025 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.
@@ -22,7 +22,7 @@ package xyz.zhouxy.plusone.commons.function;
* <p>
* 接收一个参数,返回一个布尔值,可抛出异常。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.Predicate
*/
@@ -34,6 +34,7 @@ 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 2024-2025 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.
@@ -25,7 +25,7 @@ package xyz.zhouxy.plusone.commons.function;
* @param <T> 结果类型
* @param <E> 异常类型
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see java.util.function.Supplier
*/
@@ -36,6 +36,7 @@ public interface ThrowingSupplier<T, E extends Throwable> {
* 获取一个结果,允许抛出异常。
*
* @return 结果
* @throws E 允许抛出的异常
*/
T get() throws E;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +25,7 @@ import java.util.function.BiFunction;
* <p>
* 接受类型为 T 和 U 的两个参数,返回 {@code Optional&lt;R&gt;} 对象。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see Optional
* @see BiFunction

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +25,7 @@ import java.util.function.Function;
* <p>
* 接受类型为 T 的参数,返回 {@code Optional&lt;R&gt;} 对象。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see Optional
* @see Function

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -39,6 +39,6 @@
* | Optional | ToOptionalFunction | Optional&lt;R&gt; apply(T) |
* </pre>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
package xyz.zhouxy.plusone.commons.function;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -33,7 +33,7 @@ import com.google.gson.stream.JsonWriter;
/**
* 包含 JSR-310 相关数据类型的 {@code TypeAdapter}
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.1.0
* @see TypeAdapter
* @see com.google.gson.GsonBuilder

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -42,7 +42,7 @@ import xyz.zhouxy.plusone.commons.util.StringTools;
* <p>
* 中国第二代居民身份证号
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see xyz.zhouxy.plusone.commons.constant.PatternConsts#CHINESE_2ND_ID_CARD_NUMBER
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -23,7 +23,7 @@ import xyz.zhouxy.plusone.commons.base.IWithIntCode;
/**
* 性别
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public enum Gender implements IWithIntCode {
UNKNOWN(0, "Unknown", "未知"),

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -24,7 +24,7 @@ import xyz.zhouxy.plusone.commons.util.StringTools;
/**
* 身份证号
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public interface IDCardNumber {

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

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.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;
@@ -31,7 +32,7 @@ import xyz.zhouxy.plusone.commons.annotation.ReaderMethod;
/**
* 带校验的字符串值对象
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*
* @deprecated 弃用。使用工厂方法创建对象,并在其中进行校验即可。
@@ -75,8 +76,8 @@ public abstract class ValidatableStringRecord<T extends ValidatableStringRecord<
* @param errorMessage 正则不匹配时的错误信息
*/
protected ValidatableStringRecord(String value, Pattern pattern, String errorMessage) {
checkArgument(Objects.nonNull(value), "The value cannot be null.");
checkArgument(Objects.nonNull(pattern), "The pattern cannot be null.");
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;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -29,7 +29,7 @@ import xyz.zhouxy.plusone.commons.collection.CollectionTools;
*
* @param <T> 内容列表的元素类型
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @see PagingAndSortingQueryParams
*/
public class PageResult<T> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2025 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.
@@ -27,7 +27,6 @@ import javax.annotation.Nullable;
import com.google.common.collect.ImmutableMap;
import xyz.zhouxy.plusone.commons.annotation.Virtual;
import xyz.zhouxy.plusone.commons.collection.CollectionTools;
import xyz.zhouxy.plusone.commons.util.RegexTools;
import xyz.zhouxy.plusone.commons.util.StringTools;
@@ -36,37 +35,86 @@ import xyz.zhouxy.plusone.commons.util.StringTools;
* 分页排序查询参数
*
* <p>
* 根据传入的 {@code size} 和 {@code pageNum}
* 提供 {@code getOffset} 方法计算 SQL 语句中 {@code offset} 的值。
* 包含三个主要的属性:
* <ul>
* <li>size - 每页显示的记录数</li>
* <li>pageNum - 当前页码</li>
* <li>orderBy - 排序条件</li>
* </ul>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* <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 static final int DEFAULT_PAGE_SIZE = 15;
private final PagingParamsBuilder pagingParamsBuilder;
private Integer size;
private Long pageNum;
private List<String> orderBy;
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;
/**
* 构造分页排序查询参数
* 创建一个 {@code PagingAndSortingQueryParams} 实例
*
* @param sortableProperties 可排序的属性。不可为空。
* @param pagingParamsBuilder
* 分页参数构造器。
* 通过 {@link #pagingParamsBuilder(int, int, Map)} 创建,同一场景下只需要共享同一个实例。
*/
public PagingAndSortingQueryParams(Map<String, String> sortableProperties) {
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);
protected PagingAndSortingQueryParams(PagingParamsBuilder pagingParamsBuilder) {
this.pagingParamsBuilder = pagingParamsBuilder;
}
// Setters
@@ -100,56 +148,38 @@ public class PagingAndSortingQueryParams {
// Setters end
/**
* 构建分页参数
*
* @return {@code PagingParams} 对象
*/
public final PagingParams buildPagingParams() {
final int sizeValue = this.size != null ? this.size : defaultSizeInternal();
final long pageNumValue = this.pageNum != null ? this.pageNum : 1L;
checkArgument(CollectionTools.isNotEmpty(this.orderBy),
"The 'orderBy' cannot be empty");
final List<SortableProperty> propertiesToSort = this.orderBy.stream()
.map(this::generateSortableProperty)
.collect(Collectors.toList());
return new PagingParams(sizeValue, pageNumValue, propertiesToSort);
}
/**
* 默认每页大小
*
* <p>NOTE: 可覆写此方法</p>
*
* @return 默认每页大小
*/
@Virtual
protected int defaultSizeInternal() {
return DEFAULT_PAGE_SIZE;
}
@Override
public String toString() {
return "PagingAndSortingQueryParams ["
+ "size=" + size
+ ", pageNum=" + pageNum
+ ", orderBy=" + orderBy
+ ", sortableProperties=" + sortableProperties
+ "]";
}
private SortableProperty generateSortableProperty(String orderByStr) {
checkArgument(StringTools.isNotBlank(orderByStr));
checkArgument(RegexTools.matches(orderByStr, SORT_STR_PATTERN));
String[] propertyNameAndOrderType = orderByStr.split("-");
checkArgument(propertyNameAndOrderType.length == 2);
/**
* 创建一个分页参数构造器
*
* @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);
}
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);
/**
* 根据当前查询参数,构建分页参数
*
* @return 分页参数
*/
public PagingParams buildPagingParams() {
return this.pagingParamsBuilder.buildPagingParams(this);
}
/**
@@ -162,7 +192,7 @@ public class PagingAndSortingQueryParams {
private final String sqlSnippet;
SortableProperty(String propertyName, String columnName, String orderType) {
private SortableProperty(String propertyName, String columnName, String orderType) {
this.propertyName = propertyName;
this.columnName = columnName;
checkArgument("ASC".equalsIgnoreCase(orderType) || "DESC".equalsIgnoreCase(orderType));
@@ -217,4 +247,47 @@ public class PagingAndSortingQueryParams {
}
}
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

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -24,7 +24,7 @@ import xyz.zhouxy.plusone.commons.model.dto.PagingAndSortingQueryParams.Sortable
/**
* 分页参数
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @see PagingAndSortingQueryParams
*/
public class PagingParams {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,7 +22,7 @@ import javax.annotation.Nullable;
/**
* 统一结果,对返回给前端的数据进行封装。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public class UnifiedResponse<T> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -19,9 +19,42 @@ package xyz.zhouxy.plusone.commons.model.dto;
import javax.annotation.Nullable;
/**
* UnifiedResponse 工厂
* {@link UnifiedResponse} 工厂类。
* 用于快速构建 {@link UnifiedResponse} 对象,默认的成功代码为 {@code 2000000}。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* <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
*/
@@ -37,6 +70,7 @@ public class UnifiedResponses {
/**
* 默认成功响应结果
*
* @param <T> data 类型
* @return {@code UnifiedResponse} 对象。
* {@code code} = "2000000", {@code message} = "SUCCESS", {@code data} = null
*/
@@ -48,6 +82,7 @@ public class UnifiedResponses {
* 使用指定 {@code message} 创建成功响应结果
*
* @param message 成功信息
* @param <T> data 类型
* @return {@code UnifiedResponse} 对象。
* {@code code} = "2000000", {@code data} = null
*/
@@ -81,6 +116,7 @@ public class UnifiedResponses {
*
* @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) {
@@ -126,6 +162,7 @@ public class UnifiedResponses {
*
* @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) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -52,15 +52,13 @@
* {@link UnifiedResponse} 对返回给前端的数据进行封装,包含 code、message、data。
*
* <p>
* 可使用 {@link UnifiedResponses} 快速构建 {@link UnifiedResponse} 对象。
* {@link UnifiedResponses} 默认的成功代码为 "2000000"
* 用户按测试类
* <a href="http://zhouxy.xyz:3000/plusone/plusone-commons/src/branch/main/src/test/java/xyz/zhouxy/plusone/commons/model/dto/CustomUnifiedResponseFactoryTests.java">CustomUnifiedResponseFactoryTests</a>
* 中所示范的,继承 {@link UnifiedResponses} 实现自己的工厂类,
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG 和工厂方法。
* 见 <a href="http://zhouxy.xyz:3000/plusone/plusone-commons/issues/22">issue#22</a>。
* {@link UnifiedResponses} 用于快速构建 {@link UnifiedResponse} 对象,默认的成功代码为 {@code 2000000}
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* <p>
* 用户可以继承 {@link UnifiedResponses} 实现自己的工厂类,
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG以及工厂方法。
*
* @author ZhouXY
*/
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.model.dto;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -19,7 +19,7 @@
* <p>
* 包含业务建模可能用到的性别、身份证等元素,也包含 DTO 相关类,如分页查询参数,响应结果,分页结果等。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.model;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -32,7 +32,7 @@ import xyz.zhouxy.plusone.commons.base.IWithIntCode;
/**
* 季度
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public enum Quarter implements IWithIntCode {
/** 第一季度 */

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -23,10 +23,12 @@ 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;
@@ -37,7 +39,7 @@ import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
/**
* 表示年份与季度
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
@Immutable
public final class YearQuarter implements Comparable<YearQuarter>, Serializable {
@@ -102,17 +104,50 @@ public final class YearQuarter implements Comparable<YearQuarter>, Serializable
*
* @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);
@SuppressWarnings("deprecation")
final int yearValue = YEAR.checkValidIntValue(date.getYear() + 1900L);
@SuppressWarnings("deprecation")
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} 实例
*
@@ -260,11 +295,9 @@ public final class YearQuarter implements Comparable<YearQuarter>, Serializable
if (quartersToAdd == 0L) {
return this;
}
long quarterCount = this.year * 4L + (this.quarter.getValue() - 1);
long calcQuarters = quarterCount + quartersToAdd; // safe overflow
int newYear = YEAR.checkValidIntValue(Math.floorDiv(calcQuarters, 4));
int newQuarter = (int) Math.floorMod(calcQuarters, 4) + 1;
return new YearQuarter(newYear, Quarter.of(newQuarter));
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) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -22,7 +22,7 @@
* 模仿 JDK 的 {@link java.time.Month} 和 {@link java.time.YearMonth}
* 实现 {@link Quarter}{@link YearQuarter},对季度进行建模。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.time;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -17,6 +17,7 @@
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 static xyz.zhouxy.plusone.commons.util.AssertTools.checkNotNull;
import java.math.BigDecimal;
@@ -37,7 +38,7 @@ import javax.annotation.Nullable;
* <p>
* 数组工具类
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public class ArrayTools {
@@ -260,7 +261,7 @@ public class ArrayTools {
*
* @throws IllegalArgumentException 当参数为空时抛出
*/
public static <T> boolean isAllElementsNotNull(final T[] arr) {
public static <T> boolean isAllElementsNotNull(@Nullable final T[] arr) {
checkArgument(arr != null, "The array cannot be null.");
return Arrays.stream(arr).allMatch(Objects::nonNull);
}
@@ -491,7 +492,7 @@ public class ArrayTools {
* @return 重复后的数组
*/
public static char[] repeat(char[] arr, int times, int maxLength) {
checkArgument(Objects.nonNull(arr));
checkArgumentNotNull(arr);
checkArgument(times >= 0,
"The number of times must be greater than or equal to zero");
checkArgument(maxLength >= 0,
@@ -526,7 +527,7 @@ public class ArrayTools {
* @return 重复后的数组
*/
public static byte[] repeat(byte[] arr, int times, int maxLength) {
checkArgument(Objects.nonNull(arr));
checkArgumentNotNull(arr);
checkArgument(times >= 0,
"The number of times must be greater than or equal to zero");
checkArgument(maxLength >= 0,
@@ -561,7 +562,7 @@ public class ArrayTools {
* @return 重复后的数组
*/
public static short[] repeat(short[] arr, int times, int maxLength) {
checkArgument(Objects.nonNull(arr));
checkArgumentNotNull(arr);
checkArgument(times >= 0,
"The number of times must be greater than or equal to zero");
checkArgument(maxLength >= 0,
@@ -596,7 +597,7 @@ public class ArrayTools {
* @return 重复后的数组
*/
public static int[] repeat(int[] arr, int times, int maxLength) {
checkArgument(Objects.nonNull(arr));
checkArgumentNotNull(arr);
checkArgument(times >= 0,
"The number of times must be greater than or equal to zero");
checkArgument(maxLength >= 0,
@@ -631,7 +632,7 @@ public class ArrayTools {
* @return 重复后的数组
*/
public static long[] repeat(long[] arr, int times, int maxLength) {
checkArgument(Objects.nonNull(arr));
checkArgumentNotNull(arr);
checkArgument(times >= 0,
"The number of times must be greater than or equal to zero");
checkArgument(maxLength >= 0,
@@ -666,7 +667,7 @@ public class ArrayTools {
* @return 重复后的数组
*/
public static float[] repeat(float[] arr, int times, int maxLength) {
checkArgument(Objects.nonNull(arr));
checkArgumentNotNull(arr);
checkArgument(times >= 0,
"The number of times must be greater than or equal to zero");
checkArgument(maxLength >= 0,
@@ -701,7 +702,7 @@ public class ArrayTools {
* @return 重复后的数组
*/
public static double[] repeat(double[] arr, int times, int maxLength) {
checkArgument(Objects.nonNull(arr));
checkArgumentNotNull(arr);
checkArgument(times >= 0,
"The number of times must be greater than or equal to zero");
checkArgument(maxLength >= 0,
@@ -750,7 +751,7 @@ public class ArrayTools {
* @param values 填充内容
*/
public static void fill(char[] a, int fromIndex, int toIndex, @Nullable char[] values) {
checkArgument(Objects.nonNull(a));
checkArgumentNotNull(a);
if (values == null || values.length == 0) {
return;
}
@@ -793,7 +794,7 @@ public class ArrayTools {
* @param values 填充内容
*/
public static void fill(byte[] a, int fromIndex, int toIndex, @Nullable byte[] values) {
checkArgument(Objects.nonNull(a));
checkArgumentNotNull(a);
if (values == null || values.length == 0) {
return;
}
@@ -836,7 +837,7 @@ public class ArrayTools {
* @param values 填充内容
*/
public static void fill(short[] a, int fromIndex, int toIndex, @Nullable short[] values) {
checkArgument(Objects.nonNull(a));
checkArgumentNotNull(a);
if (values == null || values.length == 0) {
return;
}
@@ -879,7 +880,7 @@ public class ArrayTools {
* @param values 填充内容
*/
public static void fill(int[] a, int fromIndex, int toIndex, @Nullable int[] values) {
checkArgument(Objects.nonNull(a));
checkArgumentNotNull(a);
if (values == null || values.length == 0) {
return;
}
@@ -922,7 +923,7 @@ public class ArrayTools {
* @param values 填充内容
*/
public static void fill(long[] a, int fromIndex, int toIndex, @Nullable long[] values) {
checkArgument(Objects.nonNull(a));
checkArgumentNotNull(a);
if (values == null || values.length == 0) {
return;
}
@@ -965,7 +966,7 @@ public class ArrayTools {
* @param values 填充内容
*/
public static void fill(float[] a, int fromIndex, int toIndex, @Nullable float[] values) {
checkArgument(Objects.nonNull(a));
checkArgumentNotNull(a);
if (values == null || values.length == 0) {
return;
}
@@ -1008,7 +1009,7 @@ public class ArrayTools {
* @param values 填充内容
*/
public static void fill(double[] a, int fromIndex, int toIndex, @Nullable double[] values) {
checkArgument(Objects.nonNull(a));
checkArgumentNotNull(a);
if (values == null || values.length == 0) {
return;
}
@@ -1037,6 +1038,7 @@ public class ArrayTools {
*
* @param a 要填充的数组
* @param values 填充内容
* @param <T> 元素类型
*/
public static <T> void fill(T[] a, @Nullable T[] values) {
fillInternal(a, 0, a.length, values);
@@ -1049,6 +1051,7 @@ public class ArrayTools {
* @param fromIndex 开始位置
* @param toIndex 结束位置
* @param values 填充内容
* @param <T> 元素类型
*/
public static <T> void fill(T[] a, int fromIndex, int toIndex, @Nullable T[] values) {
fillInternal(a, fromIndex, toIndex, values);
@@ -1063,7 +1066,7 @@ public class ArrayTools {
* @param values 填充内容
*/
private static <T> void fillInternal(T[] a, int fromIndex, int toIndex, @Nullable T[] values) {
checkArgument(Objects.nonNull(a));
checkArgumentNotNull(a);
if (values == null || values.length == 0) {
return;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -22,7 +22,6 @@ import java.util.function.Supplier;
import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.exception.DataNotExistsException;
import xyz.zhouxy.plusone.commons.exception.system.DataOperationResultException;
/**
* 断言工具
@@ -34,12 +33,12 @@ import xyz.zhouxy.plusone.commons.exception.system.DataOperationResultException;
* checkArgument(StringUtils.hasText(str), "The argument cannot be blank.");
* checkState(ArrayUtils.isNotEmpty(result), "The result cannot be empty.");
* checkCondition(!CollectionUtils.isEmpty(roles),
* () -> new InvalidInputException("The roles cannot be empty."));
* () -&gt; new InvalidInputException("The roles cannot be empty."));
* checkCondition(RegexTools.matches(email, PatternConsts.EMAIL),
* "must be a well-formed email address");
* </pre>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public class AssertTools {
@@ -54,7 +53,9 @@ public class AssertTools {
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition) {
checkCondition(condition, IllegalArgumentException::new);
if (!condition) {
throw new IllegalArgumentException();
}
}
/**
@@ -65,7 +66,9 @@ public class AssertTools {
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition, @Nullable String errorMessage) {
checkCondition(condition, () -> new IllegalArgumentException(errorMessage));
if (!condition) {
throw new IllegalArgumentException(errorMessage);
}
}
/**
@@ -76,7 +79,9 @@ public class AssertTools {
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition, Supplier<String> errorMessageSupplier) {
checkCondition(condition, () -> new IllegalArgumentException(errorMessageSupplier.get()));
if (!condition) {
throw new IllegalArgumentException(errorMessageSupplier.get());
}
}
/**
@@ -89,8 +94,9 @@ public class AssertTools {
*/
public static void checkArgument(boolean condition,
String errorMessageTemplate, Object... errorMessageArgs) {
checkCondition(condition,
() -> new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs)));
if (!condition) {
throw new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
@@ -106,10 +112,13 @@ public class AssertTools {
*
* @param <T> 入参类型
* @param obj 入参
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(@Nullable T obj) {
checkCondition(obj != null, IllegalArgumentException::new);
if (obj == null) {
throw new IllegalArgumentException();
}
return obj;
}
@@ -119,10 +128,13 @@ public class AssertTools {
* @param <T> 入参类型
* @param obj 入参
* @param errorMessage 异常信息
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(@Nullable T obj, String errorMessage) {
checkCondition(obj != null, () -> new IllegalArgumentException(errorMessage));
if (obj == null) {
throw new IllegalArgumentException(errorMessage);
}
return obj;
}
@@ -132,10 +144,13 @@ public class AssertTools {
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageSupplier 异常信息
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(@Nullable T obj, Supplier<String> errorMessageSupplier) {
checkCondition(obj != null, () -> new IllegalArgumentException(errorMessageSupplier.get()));
if (obj == null) {
throw new IllegalArgumentException(errorMessageSupplier.get());
}
return obj;
}
@@ -146,12 +161,14 @@ public class AssertTools {
* @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) {
checkCondition(obj != null,
() -> new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs)));
if (obj == null) {
throw new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs));
}
return obj;
}
@@ -170,7 +187,9 @@ public class AssertTools {
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition) {
checkCondition(condition, IllegalStateException::new);
if (!condition) {
throw new IllegalStateException();
}
}
/**
@@ -181,7 +200,9 @@ public class AssertTools {
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition, @Nullable String errorMessage) {
checkCondition(condition, () -> new IllegalStateException(errorMessage));
if (!condition) {
throw new IllegalStateException(errorMessage);
}
}
/**
@@ -192,7 +213,9 @@ public class AssertTools {
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition, Supplier<String> errorMessageSupplier) {
checkCondition(condition, () -> new IllegalStateException(errorMessageSupplier.get()));
if (!condition) {
throw new IllegalStateException(errorMessageSupplier.get());
}
}
/**
@@ -205,8 +228,9 @@ public class AssertTools {
*/
public static void checkState(boolean condition,
String errorMessageTemplate, Object... errorMessageArgs) {
checkCondition(condition,
() -> new IllegalStateException(String.format(errorMessageTemplate, errorMessageArgs)));
if (!condition) {
throw new IllegalStateException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
@@ -225,7 +249,9 @@ public class AssertTools {
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(@Nullable T obj) {
checkCondition(obj != null, NullPointerException::new);
if (obj == null) {
throw new NullPointerException();
}
}
/**
@@ -237,7 +263,9 @@ public class AssertTools {
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(@Nullable T obj, String errorMessage) {
checkCondition(obj != null, () -> new NullPointerException(errorMessage));
if (obj == null) {
throw new NullPointerException(errorMessage);
}
}
/**
@@ -249,7 +277,9 @@ public class AssertTools {
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(@Nullable T obj, Supplier<String> errorMessageSupplier) {
checkCondition(obj != null, () -> new NullPointerException(errorMessageSupplier.get()));
if (obj == null) {
throw new NullPointerException(errorMessageSupplier.get());
}
}
/**
@@ -263,8 +293,9 @@ public class AssertTools {
*/
public static <T> void checkNotNull(@Nullable T obj,
String errorMessageTemplate, Object... errorMessageArgs) {
checkCondition(obj != null,
() -> new NullPointerException(String.format(errorMessageTemplate, errorMessageArgs)));
if (obj == null) {
throw new NullPointerException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
@@ -285,7 +316,9 @@ public class AssertTools {
*/
public static <T> T checkExists(@Nullable T obj)
throws DataNotExistsException {
checkCondition(obj != null, DataNotExistsException::new);
if (obj == null) {
throw new DataNotExistsException();
}
return obj;
}
@@ -300,7 +333,9 @@ public class AssertTools {
*/
public static <T> T checkExists(@Nullable T obj, String errorMessage)
throws DataNotExistsException {
checkCondition(obj != null, () -> new DataNotExistsException(errorMessage));
if (obj == null) {
throw new DataNotExistsException(errorMessage);
}
return obj;
}
@@ -315,7 +350,9 @@ public class AssertTools {
*/
public static <T> T checkExists(@Nullable T obj, Supplier<String> errorMessageSupplier)
throws DataNotExistsException {
checkCondition(obj != null, () -> new DataNotExistsException(errorMessageSupplier.get()));
if (obj == null) {
throw new DataNotExistsException(errorMessageSupplier.get());
}
return obj;
}
@@ -332,8 +369,9 @@ public class AssertTools {
public static <T> T checkExists(@Nullable T obj,
String errorMessageTemplate, Object... errorMessageArgs)
throws DataNotExistsException {
checkCondition(obj != null,
() -> new DataNotExistsException(String.format(errorMessageTemplate, errorMessageArgs)));
if (obj == null) {
throw new DataNotExistsException(String.format(errorMessageTemplate, errorMessageArgs));
}
return obj;
}
@@ -347,7 +385,9 @@ public class AssertTools {
*/
public static <T> T checkExists(Optional<T> optional)
throws DataNotExistsException {
checkCondition(optional.isPresent(), DataNotExistsException::new);
if (!optional.isPresent()) {
throw new DataNotExistsException();
}
return optional.get();
}
@@ -362,7 +402,9 @@ public class AssertTools {
*/
public static <T> T checkExists(Optional<T> optional, String errorMessage)
throws DataNotExistsException {
checkCondition(optional.isPresent(), () -> new DataNotExistsException(errorMessage));
if (!optional.isPresent()) {
throw new DataNotExistsException(errorMessage);
}
return optional.get();
}
@@ -377,7 +419,9 @@ public class AssertTools {
*/
public static <T> T checkExists(Optional<T> optional, Supplier<String> errorMessageSupplier)
throws DataNotExistsException {
checkCondition(optional.isPresent(), () -> new DataNotExistsException(errorMessageSupplier.get()));
if (!optional.isPresent()) {
throw new DataNotExistsException(errorMessageSupplier.get());
}
return optional.get();
}
@@ -394,8 +438,9 @@ public class AssertTools {
public static <T> T checkExists(Optional<T> optional,
String errorMessageTemplate, Object... errorMessageArgs)
throws DataNotExistsException {
checkCondition(optional.isPresent(),
() -> new DataNotExistsException(String.format(errorMessageTemplate, errorMessageArgs)));
if (!optional.isPresent()) {
throw new DataNotExistsException(String.format(errorMessageTemplate, errorMessageArgs));
}
return optional.get();
}
@@ -403,208 +448,6 @@ public class AssertTools {
// #endregion - Exists
// ================================
// ================================
// #region - AffectedRows
// ================================
/**
* 当影响的数据量与预计不同时抛出 {@link DataOperationResultException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
*/
public static void checkAffectedRows(int expected, int actualRowCount) {
if (expected != actualRowCount) {
throw new DataOperationResultException(expected, actualRowCount);
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link DataOperationResultException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessage 异常信息
*/
public static void checkAffectedRows(int expected, int actualRowCount,
@Nullable String errorMessage) {
if (expected != actualRowCount) {
throw new DataOperationResultException(expected, actualRowCount, errorMessage);
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link DataOperationResultException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessageSupplier 异常信息
*/
public static void checkAffectedRows(int expected, int actualRowCount,
Supplier<String> errorMessageSupplier) {
if (expected != actualRowCount) {
throw new DataOperationResultException(expected, actualRowCount, errorMessageSupplier.get());
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link DataOperationResultException}。
*
* @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 DataOperationResultException(expected, actualRowCount,
String.format(errorMessageTemplate, errorMessageArgs));
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link DataOperationResultException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
*/
public static void checkAffectedRows(long expected, long actualRowCount) {
if (expected != actualRowCount) {
throw new DataOperationResultException(expected, actualRowCount);
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link DataOperationResultException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessage 异常信息
*/
public static void checkAffectedRows(long expected, long actualRowCount,
@Nullable String errorMessage) {
if (expected != actualRowCount) {
throw new DataOperationResultException(expected, actualRowCount, errorMessage);
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link DataOperationResultException}。
*
* @param expected 预期影响的行数
* @param actualRowCount 实际影响的行数
* @param errorMessageSupplier 异常信息
*/
public static void checkAffectedRows(long expected, long actualRowCount,
Supplier<String> errorMessageSupplier) {
if (expected != actualRowCount) {
throw new DataOperationResultException(expected, actualRowCount, errorMessageSupplier.get());
}
}
/**
* 当影响的数据量与预计不同时抛出 {@link DataOperationResultException}。
*
* @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 DataOperationResultException(expected, actualRowCount,
String.format(errorMessageTemplate, errorMessageArgs));
}
}
/**
* 当影响的数据量不为 1 时抛出 {@link DataOperationResultException}。
*
* @param actualRowCount 实际影响的行数
*/
public static void checkAffectedOneRow(int actualRowCount) {
checkAffectedRows(1, actualRowCount);
}
/**
* 当影响的数据量不为 1 时抛出 {@link DataOperationResultException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessage 异常信息
*/
public static void checkAffectedOneRow(int actualRowCount, String errorMessage) {
checkAffectedRows(1, actualRowCount, errorMessage);
}
/**
* 当影响的数据量不为 1 时抛出 {@link DataOperationResultException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessageSupplier 异常信息
*/
public static void checkAffectedOneRow(int actualRowCount, Supplier<String> errorMessageSupplier) {
checkAffectedRows(1, actualRowCount, errorMessageSupplier);
}
/**
* 当影响的数据量不为 1 时抛出 {@link DataOperationResultException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
*/
public static void checkAffectedOneRow(int actualRowCount,
String errorMessageTemplate, Object... errorMessageArgs) {
checkAffectedRows(1, actualRowCount, errorMessageTemplate, errorMessageArgs);
}
/**
* 当影响的数据量不为 1 时抛出 {@link DataOperationResultException}。
*
* @param result 实际影响的数据量
*/
public static void checkAffectedOneRow(long result) {
checkAffectedRows(1L, result);
}
/**
* 当影响的数据量不为 1 时抛出 {@link DataOperationResultException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessage 异常信息
*/
public static void checkAffectedOneRow(long actualRowCount, String errorMessage) {
checkAffectedRows(1L, actualRowCount, errorMessage);
}
/**
* 当影响的数据量不为 1 时抛出 {@link DataOperationResultException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessageSupplier 异常信息
*/
public static void checkAffectedOneRow(long actualRowCount, Supplier<String> errorMessageSupplier) {
checkAffectedRows(1L, actualRowCount, errorMessageSupplier);
}
/**
* 当影响的数据量不为 1 时抛出 {@link DataOperationResultException}。
*
* @param actualRowCount 实际影响的行数
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
*/
public static void checkAffectedOneRow(long actualRowCount,
String errorMessageTemplate, Object... errorMessageArgs) {
checkAffectedRows(1L, actualRowCount, errorMessageTemplate, errorMessageArgs);
}
// ================================
// #endregion - AffectedRows
// ================================
// ================================
// #region - Condition
// ================================

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.
@@ -31,7 +31,7 @@ import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
* <p>
* BigDecimal 工具类
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public class BigDecimals {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.
@@ -27,20 +27,21 @@ import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.chrono.IsoChronology;
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 <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public class DateTimeTools {
@@ -383,17 +384,47 @@ public class DateTimeTools {
*
* @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);
}
@@ -404,6 +435,7 @@ public class DateTimeTools {
* @param month 月份
* @return 季度
*/
@StaticFactoryMethod(Quarter.class)
public static Quarter getQuarter(Month month) {
return Quarter.fromMonth(month);
}
@@ -415,6 +447,7 @@ public class DateTimeTools {
* @param month 月
* @return 季度
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter getQuarter(int year, Month month) {
return YearQuarter.of(YearMonth.of(year, month));
}
@@ -425,6 +458,7 @@ public class DateTimeTools {
* @param yearMonth 年月
* @return 季度
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter getQuarter(YearMonth yearMonth) {
return YearQuarter.of(yearMonth);
}
@@ -435,6 +469,7 @@ public class DateTimeTools {
* @param date 日期
* @return 日期所在的季度
*/
@StaticFactoryMethod(YearQuarter.class)
public static YearQuarter getQuarter(LocalDate date) {
return YearQuarter.of(date);
}
@@ -649,7 +684,7 @@ public class DateTimeTools {
// ================================
// ================================
// #region - others
// #region - range
// ================================
/**
@@ -673,6 +708,53 @@ public class DateTimeTools {
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
// ================================
/**
* 判断指定年份是否为闰年
*
@@ -680,7 +762,7 @@ public class DateTimeTools {
* @return 指定年份是否为闰年
*/
public static boolean isLeapYear(int year) {
return IsoChronology.INSTANCE.isLeapYear(year);
return Year.isLeap(year);
}
// ================================

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2025 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.
@@ -26,7 +26,7 @@ import javax.annotation.Nullable;
/**
* 枚举工具类
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public final class EnumTools {
@@ -154,7 +154,7 @@ public final class EnumTools {
*/
@Nullable
public static <E extends Enum<?>> Integer checkOrdinalNullable(Class<E> enumType, @Nullable Integer ordinal) {
return checkOrdinalOrDefault(enumType, ordinal, (Integer) null);
return checkOrdinalOrDefault(enumType, ordinal, null);
}
/**
@@ -173,9 +173,10 @@ public final class EnumTools {
/**
* 校验枚举的 ordinal如果 ordinal 为 {@code null},则返回 {@code defaultValue}。
*
* @param <E> 枚举类型
* @param enumType 枚举类型
* @param ordinal The ordinal
* @param <E> 枚举类型
* @param enumType 枚举类型
* @param ordinal The ordinal
* @param defaultValue 默认值
* @return The ordinal
*/
@Nullable

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2025 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.
@@ -33,10 +33,11 @@ import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
/**
* 枚举类
*
* <p>
* 参考 <a href="https://lostechies.com/jimmybogard/2008/08/12/enumeration-classes/">Enumeration classes</a>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @deprecated 设计 Enumeration 的灵感来自于 .net 社区,因为 C&num; 的枚举不带行为。
* @author ZhouXY
* @deprecated 设计 Enumeration 的灵感来自于 .net 社区,因为 C# 的枚举不带行为。
* 但 Java 的枚举可以带行为,故大多数情况下不需要这种设计。
*/
@Deprecated

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.
@@ -30,7 +30,7 @@ import java.util.concurrent.ConcurrentHashMap;
*
* @see UUID
* @see IdWorker
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public class IdGenerator {

View File

@@ -27,7 +27,11 @@ import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.exception.system.NoAvailableMacFoundException;
/**
* Seata 提供的修改版雪花ID
* 修改版雪花 ID 生成器
*
* <p>
* 来自 <a href="https://seata.apache.org">Seata</a> 的 {@code org.apache.seata.common.util.IdWorker}
*
* <p>
* 大体思路为:
* <ol>
@@ -43,7 +47,6 @@ import xyz.zhouxy.plusone.commons.exception.system.NoAvailableMacFoundException;
* <li><a href="https://juejin.cn/post/7264387737276203065">在开源项目中看到一个改良版的雪花算法,现在它是你的了。</a></li>
* <li><a href="https://juejin.cn/post/7265516484029743138">关于若干读者,阅读“改良版雪花算法”后提出的几个共性问题的回复。</a></li>
* </ul>
*
*/
public class IdWorker {
@@ -180,7 +183,7 @@ public class IdWorker {
/**
* use lowest 10 bit of available MAC as workerId
* @return workerId
* @throws SocketException
* @throws SocketException if an I/O error occurs.
* @throws NoAvailableMacFoundException when there is no available mac found
*/
private static long generateWorkerIdBaseOnMac() throws SocketException, NoAvailableMacFoundException {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -19,7 +19,7 @@ package xyz.zhouxy.plusone.commons.util;
/**
* Joda-Time 工具类
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public class JodaTimeTools {
@@ -54,7 +54,9 @@ public class JodaTimeTools {
* @param zone 时区
* @return {@link org.joda.time.Instant} 对象
*/
public static org.joda.time.Instant toJodaInstant(java.time.LocalDateTime localDateTime, java.time.ZoneId zone) {
public static org.joda.time.Instant toJodaInstant(
java.time.LocalDateTime localDateTime,
java.time.ZoneId zone) {
return toJodaInstant(java.time.ZonedDateTime.of(localDateTime, zone));
}
@@ -92,9 +94,9 @@ public class JodaTimeTools {
* {@link org.joda.time.DateTimeZone} 对象
* 转换为 Java 中的 {@link java.time.Instant} 对象
*
* @param localDateTime
* @param zone
* @return
* @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,
@@ -135,8 +137,9 @@ public class JodaTimeTools {
public static org.joda.time.DateTime toJodaDateTime(
java.time.LocalDateTime localDateTime,
java.time.ZoneId zone) {
org.joda.time.DateTimeZone dateTimeZone = toJodaZone(zone);
return toJodaInstant(java.time.ZonedDateTime.of(localDateTime, zone).toInstant()).toDateTime(dateTimeZone);
org.joda.time.LocalDateTime jodaLocalDateTime = toJodaLocalDateTime(localDateTime);
org.joda.time.DateTimeZone jodaZone = toJodaZone(zone);
return jodaLocalDateTime.toDateTime(jodaZone);
}
/**
@@ -194,7 +197,7 @@ public class JodaTimeTools {
*
* @param instant joda-time 中的时间戳
* @param dateTimeZone joda-time 中的时区
* @return
* @return java.time 中带时区的日期时间
*/
public static java.time.ZonedDateTime toZonedDateTime(
org.joda.time.Instant instant,
@@ -218,9 +221,15 @@ public class JodaTimeTools {
* @return joda-time LocalDateTime
*/
public static org.joda.time.LocalDateTime toJodaLocalDateTime(java.time.LocalDateTime localDateTime) {
java.time.ZoneId javaZone = java.time.ZoneId.systemDefault();
org.joda.time.DateTimeZone jodaZone = toJodaZone(javaZone);
return toJodaInstant(localDateTime, javaZone).toDateTime(jodaZone).toLocalDateTime();
return new org.joda.time.LocalDateTime(
localDateTime.getYear(),
localDateTime.getMonthValue(),
localDateTime.getDayOfMonth(),
localDateTime.getHour(),
localDateTime.getMinute(),
localDateTime.getSecond(),
localDateTime.getNano() / 1_000_000 // 毫秒转纳秒
);
}
// ================================
@@ -238,9 +247,15 @@ public class JodaTimeTools {
* @return Java 8 LocalDateTime
*/
public static java.time.LocalDateTime toJavaLocalDateTime(org.joda.time.LocalDateTime localDateTime) {
org.joda.time.DateTimeZone jodaZone = org.joda.time.DateTimeZone.getDefault();
java.time.ZoneId javaZone = toJavaZone(jodaZone);
return toJavaInstant(localDateTime, jodaZone).atZone(javaZone).toLocalDateTime();
return java.time.LocalDateTime.of(
localDateTime.getYear(),
localDateTime.getMonthOfYear(),
localDateTime.getDayOfMonth(),
localDateTime.getHourOfDay(),
localDateTime.getMinuteOfHour(),
localDateTime.getSecondOfMinute(),
localDateTime.getMillisOfSecond() * 1_000_000 // 毫秒转纳秒
);
}
// ================================

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022-2025 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.
@@ -25,11 +25,13 @@ import javax.annotation.Nullable;
/**
* 数字工具类
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public class Numbers {
// ================================
// #region - sum
// ================================
/**
* 求和
@@ -131,9 +133,13 @@ public class Numbers {
return BigDecimals.sum(numbers);
}
// ================================
// #endregion
// ================================
// ================================
// #region - nullToZero
// ================================
/**
* 将 {@code null} 转换为 {@code 0}
@@ -217,7 +223,122 @@ public class Numbers {
return BigDecimals.nullToZero(val);
}
// #endregion
// ================================
// #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 2023-2025 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.
@@ -31,7 +31,7 @@ import com.google.common.annotations.Beta;
* <p>
* 提供一些 Optional 相关的方法
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
* @see Optional
* @see OptionalInt

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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,26 +17,27 @@
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.Objects;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
/**
* 随机工具类
* <p>
* 建议调用方自行维护 Random 对象
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public final class RandomTools {
private static final SecureRandom DEFAULT_SECURE_RANDOM;
static {
SecureRandom secureRandom = null;
SecureRandom secureRandom;
try {
secureRandom = SecureRandom.getInstanceStrong(); // 获取高强度安全随机数生成器
}
@@ -46,18 +47,41 @@ public final class RandomTools {
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
// ================================
/**
* 使用传入的随机数生成器,生成指定长度的字符串
*
@@ -69,20 +93,34 @@ public final class RandomTools {
* @return 随机字符串
*/
public static String randomStr(Random random, char[] sourceCharacters, int length) {
checkArgument(Objects.nonNull(random), "Random cannot be null.");
checkArgument(Objects.nonNull(sourceCharacters), "Source characters cannot be null.");
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) {
checkArgument(Objects.nonNull(sourceCharacters), "Source characters 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(ThreadLocalRandom.current(), sourceCharacters, length);
}
/**
* 使用默认的 {@code SecureRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String secureRandomStr(char[] sourceCharacters, int length) {
checkArgument(Objects.nonNull(sourceCharacters), "Source characters 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(DEFAULT_SECURE_RANDOM, sourceCharacters, length);
}
@@ -98,24 +136,139 @@ public final class RandomTools {
* @return 随机字符串
*/
public static String randomStr(Random random, String sourceCharacters, int length) {
checkArgument(Objects.nonNull(random), "Random cannot be null.");
checkArgument(Objects.nonNull(sourceCharacters), "Source characters cannot be null.");
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) {
checkArgument(Objects.nonNull(sourceCharacters), "Source characters 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(ThreadLocalRandom.current(), sourceCharacters, length);
}
/**
* 使用默认的 {@code SecureRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String secureRandomStr(String sourceCharacters, int length) {
checkArgument(Objects.nonNull(sourceCharacters), "Source characters 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(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
// ================================
/**
* 使用传入的随机数生成器,生成指定长度的字符串
*
@@ -158,6 +311,35 @@ public final class RandomTools {
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

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package xyz.zhouxy.plusone.commons.base;
package xyz.zhouxy.plusone.commons.util;
import java.util.Objects;
import java.util.function.Consumer;
@@ -27,7 +27,7 @@ import javax.annotation.Nullable;
/**
* {@link Ref} 包装了一个值表示对该值的应用
*
* <p>灵感来自于 C&num; {@code ref} 参数修饰符C&num; 允许通过以下方式将值返回给调用端</p>
* <p>灵感来自于 C# {@code ref} 参数修饰符C# 允许通过以下方式将值返回给调用端</p>
* <pre>
* void Method(ref int refArgument)
* {
@@ -67,7 +67,7 @@ import javax.annotation.Nullable;
* System.out.println(result); // Output: Return string
* </pre>
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public final class Ref<T> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.
@@ -35,7 +35,7 @@ import com.google.common.cache.LoadingCache;
/**
* 封装一些常用的正则操作,并可以缓存 {@link Pattern} 实例以复用。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
public final class RegexTools {
@@ -46,7 +46,7 @@ public final class RegexTools {
.maximumSize(MAX_CACHE_SIZE)
.build(new CacheLoader<RegexAndFlags, Pattern>() {
@SuppressWarnings("null")
public Pattern load(@Nonnull RegexAndFlags regexAndFlags) {
public Pattern load(RegexAndFlags regexAndFlags) {
return regexAndFlags.compilePattern();
}
});
@@ -363,7 +363,7 @@ public final class RegexTools {
this.flags = flags;
}
private final Pattern compilePattern() {
private Pattern compilePattern() {
return Pattern.compile(regex, flags);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -17,10 +17,10 @@
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 java.util.Objects;
import javax.annotation.Nullable;
@@ -34,7 +34,7 @@ import xyz.zhouxy.plusone.commons.constant.PatternConsts;
* <p>
* 字符串工具类。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public class StringTools {
@@ -112,7 +112,7 @@ public class StringTools {
* @return 结果
*/
public static String repeat(final String str, int times, int maxLength) {
checkArgument(Objects.nonNull(str));
checkArgumentNotNull(str);
return String.valueOf(ArrayTools.repeat(str.toCharArray(), times, maxLength));
}
@@ -177,6 +177,9 @@ public class StringTools {
*/
@Beta
public static boolean isURL(@Nullable final String cs) {
if (cs == null) {
return false;
}
try {
new URL(cs);
} catch (MalformedURLException e) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.
@@ -32,7 +32,7 @@ import javax.annotation.Nullable;
/**
* TreeBuilder
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
* @since 1.0.0
*/
public class TreeBuilder<T, TSubTree extends T, TIdentity> {
@@ -76,6 +76,7 @@ public class TreeBuilder<T, TSubTree extends T, TIdentity> {
* 注意避免 nodes 中的元素产生变化所带来的意料之外的影响。</b>
*
* @param nodes 平铺的节点列表
* @return 构造结果
*/
public List<T> buildTree(Collection<T> nodes) {
checkNotNull(nodes);
@@ -93,6 +94,7 @@ public class TreeBuilder<T, TSubTree extends T, TIdentity> {
* 若为 {@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);

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

@@ -1,5 +1,5 @@
/*
* Copyright 2025 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.
@@ -17,9 +17,10 @@
/**
* <h2>工具类</h2>
* <p>
* 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、ID 生成器({@link IdGenerator})及其它实用工具类。
* 包含树构建器({@link TreeBuilder})、断言工具({@link AssertTools})、
* ID 生成器({@link IdGenerator})及其它实用工具类。
*
* @author <a href="http://zhouxy.xyz:3000/ZhouXY108">ZhouXY</a>
* @author ZhouXY
*/
@ParametersAreNonnullByDefault
package xyz.zhouxy.plusone.commons.util;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.

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

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -38,7 +38,6 @@ public class InvalidInputExceptionTests {
throw InvalidInputException.Type.CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS.create();
});
assertSame(InvalidInputException.Type.CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS, e.getType());
assertEquals(InvalidInputException.Type.CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS.getCode(), e.getTypeCode());
assertEquals(InvalidInputException.Type.CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS.getDefaultMessage(), e.getMessage());
assertNull(e.getCause());
}
@@ -50,7 +49,6 @@ public class InvalidInputExceptionTests {
throw InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS.create(message);
});
assertSame(InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS, e.getType());
assertEquals(InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@@ -62,7 +60,6 @@ public class InvalidInputExceptionTests {
throw InvalidInputException.Type.PICTURE_CONTAINS_ILLEGAL_INFORMATION.create(message);
});
assertSame(InvalidInputException.Type.PICTURE_CONTAINS_ILLEGAL_INFORMATION, e.getType());
assertEquals(InvalidInputException.Type.PICTURE_CONTAINS_ILLEGAL_INFORMATION.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}
@@ -77,7 +74,6 @@ public class InvalidInputExceptionTests {
});
assertSame(InvalidInputException.Type.INFRINGE_COPYRIGHT, e.getType());
assertEquals(InvalidInputException.Type.INFRINGE_COPYRIGHT.getCode(), e.getTypeCode());
log.info("{}", e.getMessage());
assertEquals(nfe.toString(), e.getMessage());
assertSame(nfe, e.getCause());
@@ -92,7 +88,6 @@ public class InvalidInputExceptionTests {
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}
@@ -106,7 +101,6 @@ public class InvalidInputExceptionTests {
throw InvalidInputException.Type.CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS.create(message, nfe);
});
assertSame(InvalidInputException.Type.CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS, e.getType());
assertEquals(InvalidInputException.Type.CONTAINS_ILLEGAL_AND_MALICIOUS_LINKS.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertSame(nfe, e.getCause());
}
@@ -120,7 +114,6 @@ public class InvalidInputExceptionTests {
throw InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS.create(message, nfe);
});
assertSame(InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS, e.getType());
assertEquals(InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertSame(nfe, e.getCause());
}
@@ -134,7 +127,6 @@ public class InvalidInputExceptionTests {
throw InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS.create(message, npe);
});
assertSame(InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS, e.getType());
assertEquals(InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@@ -148,7 +140,6 @@ public class InvalidInputExceptionTests {
throw InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS.create(message, nfe);
});
assertSame(InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS, e.getType());
assertEquals(InvalidInputException.Type.CONTAINS_ILLEGAL_WORDS.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}
@@ -167,7 +158,6 @@ public class InvalidInputExceptionTests {
throw new InvalidInputException();
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
assertEquals(InvalidInputException.Type.DEFAULT.getDefaultMessage(), e.getMessage());
assertNull(e.getCause());
}
@@ -179,7 +169,6 @@ public class InvalidInputExceptionTests {
throw new InvalidInputException(message);
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@@ -191,7 +180,6 @@ public class InvalidInputExceptionTests {
throw new InvalidInputException(message);
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}
@@ -206,7 +194,6 @@ public class InvalidInputExceptionTests {
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
log.info("{}", e.getMessage());
assertEquals(nfe.toString(), e.getMessage());
assertSame(nfe, e.getCause());
@@ -221,7 +208,6 @@ public class InvalidInputExceptionTests {
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}
@@ -235,7 +221,6 @@ public class InvalidInputExceptionTests {
throw new InvalidInputException(message, nfe);
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertSame(nfe, e.getCause());
}
@@ -249,7 +234,6 @@ public class InvalidInputExceptionTests {
throw new InvalidInputException(message, nfe);
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertSame(nfe, e.getCause());
}
@@ -263,7 +247,6 @@ public class InvalidInputExceptionTests {
throw new InvalidInputException(message, npe);
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@@ -277,7 +260,6 @@ public class InvalidInputExceptionTests {
throw new InvalidInputException(message, nfe);
});
assertSame(InvalidInputException.Type.DEFAULT, e.getType());
assertEquals(InvalidInputException.Type.DEFAULT.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 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.
@@ -41,7 +41,6 @@ public class ParsingFailureExceptionTests {
throw ParsingFailureException.DATE_TIME_PARSING_FAILURE.create();
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getDefaultMessage(), e.getMessage());
assertNull(e.getCause());
}
@@ -53,7 +52,6 @@ public class ParsingFailureExceptionTests {
throw ParsingFailureException.JSON_PARSING_FAILURE.create(message);
});
assertSame(ParsingFailureException.Type.JSON_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.Type.JSON_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@@ -65,7 +63,6 @@ public class ParsingFailureExceptionTests {
throw ParsingFailureException.XML_PARSING_FAILURE.create(message);
});
assertSame(ParsingFailureException.XML_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.XML_PARSING_FAILURE.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}
@@ -80,7 +77,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.NUMBER_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getCode(), e.getTypeCode());
log.info("{}", e.getMessage());
assertEquals(nfe.toString(), e.getMessage());
assertSame(nfe, e.getCause());
@@ -95,7 +91,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.NUMBER_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}
@@ -109,7 +104,6 @@ public class ParsingFailureExceptionTests {
throw ParsingFailureException.NUMBER_PARSING_FAILURE.create(message, nfe);
});
assertSame(ParsingFailureException.NUMBER_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertSame(nfe, e.getCause());
}
@@ -123,7 +117,6 @@ public class ParsingFailureExceptionTests {
throw ParsingFailureException.DATE_TIME_PARSING_FAILURE.create(message, nfe);
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertSame(nfe, e.getCause());
}
@@ -137,7 +130,6 @@ public class ParsingFailureExceptionTests {
throw ParsingFailureException.DATE_TIME_PARSING_FAILURE.create(message, npe);
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@@ -151,7 +143,6 @@ public class ParsingFailureExceptionTests {
throw ParsingFailureException.DATE_TIME_PARSING_FAILURE.create(message, nfe);
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}
@@ -175,7 +166,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(dtpe.getMessage(), e.getMessage());
assertSame(dtpe, e.getCause());
}
@@ -189,7 +179,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getDefaultMessage(), e.getMessage());
assertNull(e.getCause());
}
@@ -206,7 +195,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertSame(dtpe, e.getCause());
}
@@ -223,7 +211,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertSame(dtpe, e.getCause());
}
@@ -238,7 +225,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@@ -253,7 +239,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.DATE_TIME_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.DATE_TIME_PARSING_FAILURE.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}
@@ -277,7 +262,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.NUMBER_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(dtpe.getMessage(), e.getMessage());
assertSame(dtpe, e.getCause());
}
@@ -291,7 +275,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.NUMBER_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getDefaultMessage(), e.getMessage());
assertNull(e.getCause());
}
@@ -308,7 +291,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.NUMBER_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertSame(dtpe, e.getCause());
}
@@ -325,7 +307,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.NUMBER_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertSame(dtpe, e.getCause());
}
@@ -340,7 +321,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.NUMBER_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getCode(), e.getTypeCode());
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@@ -355,7 +335,6 @@ public class ParsingFailureExceptionTests {
});
assertSame(ParsingFailureException.NUMBER_PARSING_FAILURE, e.getType());
assertEquals(ParsingFailureException.NUMBER_PARSING_FAILURE.getCode(), e.getTypeCode());
assertNull(e.getMessage());
assertNull(e.getCause());
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2025 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.

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