13 Commits

Author SHA1 Message Date
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
16 changed files with 1506 additions and 10014 deletions

60
NOTICE Normal file
View File

@@ -0,0 +1,60 @@
Plusone Commons
Copyright 2022-present ZhouXY108
This product includes software developed at
Plusone Commons (http://gitea.zhouxy.xyz/plusone/plusone-commons).
===========================================================================
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.

View File

@@ -209,9 +209,34 @@ throw LoginException.Type.TOKEN_TIMEOUT.create();
分页结果可以存放到 `PageResult` 中,作为出参。
#### 2. UnifiedResponse
UnifiedResponse 对返回给前端的数据进行封装,包含 `code`、`message`、`data。`
可使用 `UnifiedResponses` 快速构建 `UnifiedResponse` 对象。 `UnifiedResponses` 默认的成功代码为 "2000000" 用户按测试类 `CustomUnifiedResponseFactoryTests` 中所示范的,继承 `UnifiedResponses` 实现自己的工厂类, 自定义 `SUCCESS_CODE` 和 `DEFAULT_SUCCESS_MSG` 和工厂方法。 见 [issue#22](http://gitea.zhouxy.xyz/plusone/plusone-commons/issues/22)。
`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)
## 八、time - 时间 API
### 1. 季度

View File

@@ -9,6 +9,7 @@
"aliyun",
"baomidou",
"Batis",
"buildmetadata",
"Consolas",
"cspell",
"databind",

View File

@@ -0,0 +1,254 @@
/*
* Copyright 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.collection;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
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.Nonnull;
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 ZhouXY108 <luquanlion@outlook.com>
* @since 1.1.0
*/
@Beta
public class MapModifier<K, V> {
@Nonnull
private Consumer<Map<K, V>> operators;
/**
* 创建一个空的 MapModifier
*/
public MapModifier() {
this.operators = m -> {
// do nothing
};
}
/**
* 添加一个键值对。
*
* <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));
}
/**
* 添加多个键值对。
*
* <p>
* <b>注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。</b>
*
* @param entries 要添加的键值对集合
* @return MapModifier
*/
@SafeVarargs
public final MapModifier<K, V> putAll(Map.Entry<? extends K, ? extends V>... entries) {
if (entries.length == 0) {
return this;
}
return addOperationInternal(map -> {
for (Map.Entry<? extends K, ? extends V> entry : entries) {
map.put(entry.getKey(), entry.getValue());
}
});
}
/**
* 当 {@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) {
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) {
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}
* @return 修改后的 {@code map}。当入参是 {@code null} 时,返回 {@code null}。
*/
public <T extends Map<K, V>> void modify(@Nullable T map) {
if (map != null) {
this.operators.accept(map);
}
}
/**
* 修改 {@code map}
*
* @param mapSupplier {@code map} 的 {@link Supplier}
* @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 Map}
*
* @return 不可变的 {@code Map}
*/
public Map<K, V> getUnmodifiableMap() {
return Collections.unmodifiableMap(getAndModify(HashMap::new));
}
private MapModifier<K, V> addOperationInternal(Consumer<Map<K, V>> operator) {
this.operators = this.operators.andThen(operator);
return this;
}
}

View File

@@ -0,0 +1,275 @@
/*
* Copyright 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.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 ZhouXY108 <luquanlion@outlook.com>
* @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

@@ -19,7 +19,40 @@ package xyz.zhouxy.plusone.commons.model.dto;
import javax.annotation.Nullable;
/**
* UnifiedResponse 工厂
* {@link UnifiedResponse} 工厂类。
* 用于快速构建 {@link UnifiedResponse} 对象,默认的成功代码为 {@code 2000000}。
*
* <p>
* 用户可以继承 {@link UnifiedResponses} 实现自己的工厂类,
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG以及工厂方法。
* 如下所示:
* <pre>
* // 自定义工厂类
* public static class CustomUnifiedResponses extends UnifiedResponses {
*
* public static final String SUCCESS_CODE = "000";
* public static final String DEFAULT_SUCCESS_MSG = "成功";
*
* public static <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
* </pre>
* 见 <a href="http://zhouxy.xyz:3000/plusone/plusone-commons/issues/22">issue#22</a>。
*
* @author ZhouXY108 <luquanlion@outlook.com>
* @since 1.0.0

View File

@@ -52,13 +52,11 @@
* {@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}
*
* <p>
* 用户可以继承 {@link UnifiedResponses} 实现自己的工厂类,
* 自定义 SUCCESS_CODE 和 DEFAULT_SUCCESS_MSG以及工厂方法。
*
* @author ZhouXY108 <luquanlion@outlook.com>
*/

View File

@@ -27,7 +27,11 @@ import javax.annotation.Nullable;
import xyz.zhouxy.plusone.commons.exception.system.NoAvailableMacFoundException;
/**
* Seata 提供的修改版雪花ID
* 修改版雪花 ID 生成器
*
* <p>
* 来自 Seata (https://seata.apache.org) 的 {@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 {

View File

@@ -24,10 +24,11 @@ import java.security.SecureRandom;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
/**
* 随机工具类
* <p>
* 建议调用方自行维护 Random 对象
*
* @author ZhouXY108 <luquanlion@outlook.com>
*/
@@ -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
// ================================
/**
* 使用传入的随机数生成器,生成指定长度的字符串
*
@@ -75,12 +99,26 @@ public final class RandomTools {
return randomStrInternal(random, sourceCharacters, length);
}
/**
* 使用当前线程的 {@code ThreadLocalRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String randomStr(char[] sourceCharacters, int length) {
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
return randomStrInternal(ThreadLocalRandom.current(), sourceCharacters, length);
}
/**
* 使用默认的 {@code SecureRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String secureRandomStr(char[] sourceCharacters, int length) {
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
@@ -104,18 +142,131 @@ public final class RandomTools {
return randomStrInternal(random, sourceCharacters, length);
}
/**
* 使用当前线程的 {@code ThreadLocalRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String randomStr(String sourceCharacters, int length) {
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
return randomStrInternal(ThreadLocalRandom.current(), sourceCharacters, length);
}
/**
* 使用默认的 {@code SecureRandom},生成指定长度的字符串
*
* @param sourceCharacters 字符池。字符串的字符将在数组中选,不为空
* @param length 字符串长度
* @return 随机字符串
*/
public static String secureRandomStr(String sourceCharacters, int length) {
checkArgumentNotNull(sourceCharacters, "Source characters cannot be null.");
checkArgument(length >= 0, "The length should be greater than or equal to zero.");
return randomStrInternal(DEFAULT_SECURE_RANDOM, sourceCharacters, length);
}
// ================================
// #endregion - randomStr
// ================================
// ================================
// #region - randomInt
// ================================
/**
* 使用传入的随机数生成器,生成随机整数
*
* @param 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 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 +309,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

@@ -73,11 +73,11 @@ public class ZipTools {
if (input == null) {
return null;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (DeflaterOutputStream dos = new DeflaterOutputStream(baos, new Deflater(level))) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (DeflaterOutputStream dos = new DeflaterOutputStream(out, new Deflater(level))) {
dos.write(input);
dos.finish();
return baos.toByteArray();
return out.toByteArray();
}
}
@@ -94,11 +94,11 @@ public class ZipTools {
if (input == null) {
return null;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InflaterOutputStream dos = new InflaterOutputStream(baos, new Inflater())) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InflaterOutputStream dos = new InflaterOutputStream(out, new Inflater())) {
dos.write(input);
dos.finish();
return baos.toByteArray();
return out.toByteArray();
}
}

View File

@@ -0,0 +1,316 @@
/*
* Copyright 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.collection;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
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));
// 创建一个有初始化数据的不可变的 {@code 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 putAll_entries() {
Map<String, String> entries = new HashMap<String, String>() {
{
put("key1", "value1");
put("key2", "value2");
}
};
Map<String, String> map = new MapModifier<String, String>()
.putAll(new SimpleEntry<>("key1", "value1"),
new SimpleEntry<>("key2", "value2"))
.getAndModify(HashMap::new);
assertEquals(entries, map);
new MapModifier<String, String>()
.putAll()
.putAll(new SimpleEntry<>("key2", "newValue2"),
new SimpleEntry<>("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);
assertTrue(map.containsKey("key1"));
assertEquals("value1", map.get("key1"));
}
@Test
void computeIfPresent_keyAndBiFunction() {
Map<String, String> map = new HashMap<String, String>() {{
put("key1", "value1");
}};
new MapModifier<String, String>()
.computeIfPresent("key1", (k, v) -> k + v)
.computeIfPresent("key2", (k, v) -> k + v)
.modify(map);
assertEquals(new HashMap<String, String>() {{
put("key1", "key1value1");
}}, map);
}
@Test
void remove() {
Map<String, String> map = new HashMap<String, String>() {{
put("key1", "value1");
put("key2", "value2");
}};
new MapModifier<String, String>()
.remove("key2")
.modify(map);
assertEquals(new HashMap<String, String>() {{
put("key1", "value1");
}}, map);
}
@Test
void clear() {
Map<String, String> map = new HashMap<String, String>() {{
put("key1", "value1");
put("key2", "value2");
}};
new MapModifier<String, String>()
.clear()
.modify(map);
assertTrue(map.isEmpty());
}
@Getter
static class SimpleEntry<K, V> implements Map.Entry<K, V> {
private final K key;
private final V value;
public SimpleEntry(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public V setValue(@Nullable V value) {
throw new UnsupportedOperationException("Unimplemented method 'setValue'");
}
}
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright 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.model;
import static org.apache.commons.lang3.ObjectUtils.compare;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SemVerTests {
@ParameterizedTest
@ValueSource(strings = {
"25.10.17",
"25.10.17.11",
"25.10.17.11.38",
"25.10.17-RC1",
"25.10.17.11-RC1",
"25.10.17.11.38-RC1",
"25.10.17+build.a20251017.1",
"25.10.17.11+build.a20251017.1",
"25.10.17.11.38+build.a20251017.1",
"25.10.17-RC1+build.a20251017.1",
"25.10.17.11-RC1+build.a20251017.1",
"25.10.17.11.38-RC1+build.a20251017.1",
})
void test_of_success(String value) {
assertDoesNotThrow(() -> SemVer.of(value));
}
@ParameterizedTest
@ValueSource(strings = {
"25",
"25.10",
"x.10.17",
"25.x.17",
"25.10.x",
"25.10.17.11.38.20",
"025.10.17",
"25.010.17",
"25.10.017",
})
void test_of_wrongValue(String value) {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> SemVer.of(value));
assertEquals("版本号格式错误", e.getMessage());
}
@ParameterizedTest
@ValueSource(strings = {
"25.1.1",
"25.1.1-RC1",
"25.1.1+build.a20250101.1",
"25.1.1-RC1+build.a20250101.1",
})
void compareTo_Major(String version_25_x_x) { // NOSONAR sonarqube(java:S117)
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30")) > 0);
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30.1")) > 0);
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30-RC2")) > 0);
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30+build.z20241230.1")) > 0);
assertTrue(compare(SemVer.of(version_25_x_x), SemVer.of("24.12.30-RC2+build.z20241230.1")) > 0);
}
@ParameterizedTest
@ValueSource(strings = {
"25.10.1",
"25.10.1-RC1",
"25.10.1+build.a20251001.1",
"25.10.1-RC1+build.a20251001.1",
})
void compareTo_Minor(String version_25_10_x) { // NOSONAR sonarqube(java:S117)
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30")) > 0);
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30.1")) > 0);
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30-RC2")) > 0);
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30+build.z20250930.1")) > 0);
assertTrue(compare(SemVer.of(version_25_10_x), SemVer.of("25.9.30-RC2+build.z20250930.1")) > 0);
}
@ParameterizedTest
@ValueSource(strings = {
"25.10.17",
"25.10.17-RC1",
"25.10.17+build.a20251017.1",
"25.10.17-RC1+build.a20251017.1",
})
void compareTo_Patch(String version_25_10_17) { // NOSONAR sonarqube(java:S117)
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16")) > 0);
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16.1")) > 0);
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16-RC2")) > 0);
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16+build.z20251016.2")) > 0);
assertTrue(compare(SemVer.of(version_25_10_17), SemVer.of("25.10.16-RC2+build.z20251016.2")) > 0);
}
@Test
void compareTo_MoreVersionNumber() {
assertTrue(compare(SemVer.of("25.10.17.1"), SemVer.of("25.10.17")) > 0);
assertTrue(compare(SemVer.of("25.10.17.11"), SemVer.of("25.10.17.1")) > 0);
assertEquals(0, compare(SemVer.of("25.10.17.11"), SemVer.of("25.10.17.11")));
assertTrue(compare(SemVer.of("25.10.17.11.1"), SemVer.of("25.10.17.11")) > 0);
assertTrue(compare(SemVer.of("25.10.17.11.38"), SemVer.of("25.10.17.11.1")) > 0);
assertEquals(0, compare(SemVer.of("25.10.17.11.38"), SemVer.of("25.10.17.11.38")));
}
@Test
void compareTo_PreReleaseVersion() {
// 先行版的优先级低于相关联的标准版本
assertTrue(compare(SemVer.of("25.10.17"), SemVer.of("25.10.17-0")) > 0);
assertTrue(compare(SemVer.of("25.10.17"), SemVer.of("25.10.17-RC1")) > 0);
// 只有数字的标识符以数值高低比较
assertAll(
() -> assertTrue(compare("25.10.17-RC.11", "25.10.17-RC.2") < 0),
() -> assertTrue(compare(SemVer.of("25.10.17-RC.11"), SemVer.of("25.10.17-RC.2")) > 0)
);
// 纯数字优先级低于非数字
assertAll(
() -> assertTrue(compare("25.10.17-999", "25.10.17-A") < 0),
() -> assertTrue(compare(SemVer.of("25.10.17-A.A"), SemVer.of("25.10.17-A.99")) > 0)
);
SemVer[] versions = {
SemVer.of("25.10.17-a"),
SemVer.of("25.10.17-aa"),
SemVer.of("25.10.17-A"),
SemVer.of("25.10.17-AA"),
SemVer.of("25.10.17--"),
SemVer.of("25.10.17-999"),
SemVer.of("25.10.17-z"),
SemVer.of("25.10.17-zz"),
SemVer.of("25.10.17-Z"),
SemVer.of("25.10.17-ZZ"),
};
assertArrayEquals(
new SemVer[] {
// 纯数字优先级低于非数字
SemVer.of("25.10.17-999"),
// 有字母或连接号时逐字符以 ASCII 的排序比较
SemVer.of("25.10.17--"),
SemVer.of("25.10.17-A"),
SemVer.of("25.10.17-AA"),
SemVer.of("25.10.17-Z"),
SemVer.of("25.10.17-ZZ"),
SemVer.of("25.10.17-a"),
SemVer.of("25.10.17-aa"),
SemVer.of("25.10.17-z"),
SemVer.of("25.10.17-zz"),
},
Arrays.stream(versions)
.sorted()
.toArray(SemVer[]::new));
// 若开头的标识符都相同时,栏位比较多的先行版本号优先级比较高
assertAll(
() -> assertTrue(compare(SemVer.of("25.10.17-ABC.DEF.1"), SemVer.of("25.10.17-ABC.DEF")) > 0),
() -> assertTrue(compare(SemVer.of("25.10.17-ABC.DEF.G"), SemVer.of("25.10.17-ABC.DEF")) > 0));
}
@Test
void compareTo_ignoreBuildMeta() {
assertNotEquals(SemVer.of("25.10.17"), SemVer.of("25.10.17+build.20251017.01"));
assertTrue(compare(SemVer.of("25.10.17"), SemVer.of("25.10.17+build.20251017.01")) == 0); // NOSONAR sonarqube(java:S5785)
assertNotEquals(SemVer.of("25.10.17+build.20251017.02"), SemVer.of("25.10.17+build.20251017.01"));
assertTrue(compare(SemVer.of("25.10.17+build.20251017.02"), SemVer.of("25.10.17+build.20251017.01")) == 0); // NOSONAR sonarqube(java:S5785)
assertNotEquals(SemVer.of("25.10.17-ABC.DEF"), SemVer.of("25.10.17-ABC.DEF+build.20251017.01"));
assertTrue(compare(SemVer.of("25.10.17-ABC.DEF"), SemVer.of("25.10.17-ABC.DEF+build.20251017.01")) == 0); // NOSONAR sonarqube(java:S5785)
assertNotEquals(SemVer.of("25.10.17-ABC.DEF+build.20251017.02"), SemVer.of("25.10.17-ABC.DEF+build.20251017.01"));
assertTrue(compare(SemVer.of("25.10.17-ABC.DEF+build.20251017.02"), SemVer.of("25.10.17-ABC.DEF+build.20251017.01")) == 0); // NOSONAR sonarqube(java:S5785)
}
@ParameterizedTest
@ValueSource(strings = {
"25.10.17",
"25.10.17-ABC.DEF.1",
"25.10.17+build.20251017.a2",
"25.10.17-ABC.DEF.1+build.20251017.a2",
})
void test_equals(String value) {
final SemVer v1 = SemVer.of(value);
final SemVer v2 = SemVer.of(value);
assertTrue(v1.compareTo(v1) == 0); // NOSONAR sonarqube(java:S5785)
assertTrue(v1.compareTo(v2) == 0); // NOSONAR sonarqube(java:S5785)
assertEquals(v1, v1);
assertEquals(v1.hashCode(), v2.hashCode());
assertEquals(v1, v2);
}
@Test
void test_equals_null() {
assertFalse(SemVer.of("25.10.17").equals(null)); // NOSONAR sonarqube(java:S5785)
}
@Test
void details() {
final String s = "25.10.17-ABC.DEF.1+build.20251017.02";
final SemVer v = SemVer.of(s);
assertEquals(25, v.getMajor());
assertEquals(10, v.getMinor());
assertEquals(17, v.getPatch());
assertEquals("ABC.DEF.1", v.getPreReleaseVersion());
assertEquals(s, v.getValue());
assertEquals("v" + s, v.toString());
assertEquals("build.20251017.02", v.getBuildMetadata());
}
}

View File

@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Constructor;
import java.security.SecureRandom;
@@ -30,6 +31,8 @@ import java.util.Random;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import com.google.common.collect.Range;
@SuppressWarnings("null")
public class RandomToolsTests {
@@ -56,13 +59,16 @@ public class RandomToolsTests {
@Test
public void randomStr_NullSourceCharacters_ThrowsException() {
assertThrows(IllegalArgumentException.class, () -> RandomTools.randomStr(random, (char[]) null, 5));
assertThrows(IllegalArgumentException.class, () -> RandomTools.randomStr(random, (String) null, 5));
assertThrows(IllegalArgumentException.class,
() -> RandomTools.randomStr(random, (char[]) null, 5));
assertThrows(IllegalArgumentException.class,
() -> RandomTools.randomStr(random, (String) null, 5));
}
@Test
public void randomStr_NegativeLength_ThrowsException() {
assertThrows(IllegalArgumentException.class, () -> RandomTools.randomStr(random, sourceCharactersArray, -1));
assertThrows(IllegalArgumentException.class,
() -> RandomTools.randomStr(random, sourceCharactersArray, -1));
}
@Test
@@ -107,6 +113,50 @@ public class RandomToolsTests {
assertEquals(5, result.length());
}
@Test
public void randomInt_WithMinAndMax() {
for (int i = 0; i < 1000; i++) {
int r = RandomTools.randomInt(random, -2, 3);
assertTrue(r >= -2 && r < 3);
}
}
@Test
public void randomInt_WithClosedOpenRange() {
Range<Integer> co = Range.closedOpen(-2, 3);
for (int i = 0; i < 1000; i++) {
int rco = RandomTools.randomInt(random, co);
assertTrue(rco >= -2 && rco < 3);
}
}
@Test
public void randomInt_WithClosedRange() {
Range<Integer> cc = Range.closed(-2, 3);
for (int i = 0; i < 1000; i++) {
int rcc = RandomTools.randomInt(random, cc);
assertTrue(rcc >= -2 && rcc <= 3);
}
}
@Test
public void randomInt_WithOpenClosedRange() {
Range<Integer> oc = Range.openClosed(-2, 3);
for (int i = 0; i < 1000; i++) {
int roc = RandomTools.randomInt(random, oc);
assertTrue(roc > -2 && roc <= 3);
}
}
@Test
public void randomInt_WithOpenRange() {
Range<Integer> oo = Range.open(-2, 3);
for (int i = 0; i < 1000; i++) {
int roo = RandomTools.randomInt(random, oo);
assertTrue(roo > -2 && roo < 3);
}
}
@Test
void test_constructor_isNotAccessible_ThrowsIllegalStateException() {
Constructor<?>[] constructors = RandomTools.class.getDeclaredConstructors();

View File

@@ -1,3 +1,19 @@
/*
* Copyright 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.util;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -61,7 +77,7 @@ public class ZipToolsTests {
}
@Test
void zip_WithWrongLevel() throws IOException, DataFormatException {
void zip_WithWrongLevel() {
Random random = new Random();
final int levelGtMax = random.nextInt() + 9;
assertThrows(IllegalArgumentException.class, () -> ZipTools.zip(bytes, levelGtMax));

View File

@@ -52,6 +52,8 @@
<jasypt.version>1.9.3</jasypt.version>
<jbcrypt.version>0.4</jbcrypt.version>
<minio.version>8.6.0</minio.version>
<lombok.version>1.18.36</lombok.version>
<hutool.version>5.8.37</hutool.version>
@@ -146,22 +148,27 @@
<version>${gson.version}</version>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- H2 测试数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- Query DSL -->
<dependency>
<groupId>com.querydsl</groupId>
@@ -169,6 +176,7 @@
<version>${querydsl.version}</version>
</dependency>
<!-- Byte Buddy 字节码工具 -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
@@ -188,22 +196,34 @@
<version>${poi.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${java-jwt.version}</version>
</dependency>
<!-- jasypt 加密解密 -->
<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt</artifactId>
<version>${jasypt.version}</version>
</dependency>
<!-- Bcrypt是一种用于密码哈希的加密算法基于Blowfish算法 -->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>${jbcrypt.version}</version>
</dependency>
<!-- MinIO -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>