4 Commits

Author SHA1 Message Date
aea876a5f2 docs: 添加 SemVer 规范链接
在类注释中添加了指向 Semantic Versioning 2.0.0 规范的链接。
2025-10-13 21:26:12 +08:00
ef61ecc2df feat(model): 添加 SemVer 表示语义版本号
新增 `SemVer` 类用于解析和比较语义版本号,支持版本号的解析、比较、序列化等功能。

该实现遵循语义化版本规范(SemVer 2.0.0),包括主版本号、次版本号、修订号、先行版本号和构建元数据的处理。

同时添加了基础的单元测试,验证版本号之间的排序逻辑是否符合预期。

后续需完善正则表达式优化、补充完整单元测试以及相关文档说明。
2025-10-13 21:26:12 +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
11 changed files with 10562 additions and 58 deletions

View File

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

View File

@@ -0,0 +1,211 @@
/*
* 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.Arrays;
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;
// TODO [优化] 优化正则表达式
// TODO [补充] 完善单元测试
// TODO [doc] javadoc、README.md
/**
* 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;
private final String[] preReleaseVersion;
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*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?";
private static final String BUILD_METADATA = "(?:\\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?";
private static final Pattern PATTERN = Pattern.compile(
"^" + VERSION_NUMBERS + PRE_RELEASE_VERSION + BUILD_METADATA + "$");
private SemVer(String value, int[] versionNumbers, @Nullable String[] preReleaseVersion, @Nullable String buildMetadata) {
this.value = value;
this.versionNumbers = versionNumbers;
this.preReleaseVersion = preReleaseVersion;
this.buildMetadata = buildMetadata;
}
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();
final String[] preReleaseVersion = preReleaseVersionPart != null
? Splitter.on('.').splitToStream(preReleaseVersionPart).toArray(String[]::new)
: null;
return new SemVer(value, versionNumbers, preReleaseVersion, buildMetadataPart);
}
public int getMajor() {
return this.versionNumbers[0];
}
public int getMinor() {
return this.versionNumbers[1];
}
public int getPatch() {
return this.versionNumbers[2];
}
public String getBuildMetadata() {
return buildMetadata;
}
@Override
public int compareTo(@Nullable SemVer that) {
if (that == null) {
return 1;
}
int result = compareVersionNumbers(that);
if (result != 0) {
return result;
}
return comparePreReleaseVersion(that);
}
public String getValue() {
return value;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(versionNumbers);
result = prime * result + Arrays.hashCode(preReleaseVersion);
result = prime * result + Objects.hash(value, buildMetadata);
return result;
}
@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) && Arrays.equals(versionNumbers, other.versionNumbers)
&& Arrays.equals(preReleaseVersion, other.preReleaseVersion)
&& Objects.equals(buildMetadata, other.buildMetadata);
}
@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) {
final String[] preReleaseVersionOfThis = this.preReleaseVersion;
final String[] preReleaseVersionOfThat = that.preReleaseVersion;
byte thisWithoutPreReleaseVersionFlag = preReleaseVersionOfThis == null ? (byte) 1 : (byte) 0;
byte thatWithoutPreReleaseVersionFlag = preReleaseVersionOfThat == null ? (byte) 1 : (byte) 0;
if ((thisWithoutPreReleaseVersionFlag | thatWithoutPreReleaseVersionFlag) == 1) {
return thisWithoutPreReleaseVersionFlag - thatWithoutPreReleaseVersionFlag;
}
@SuppressWarnings("null")
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(@Nullable String str) {
if (str == null || str.isEmpty()) {
return false;
}
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c < '0' || c > '9') {
return false;
}
}
return true;
}
}

View File

@@ -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;
@@ -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

@@ -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;
@@ -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;
}
@@ -1063,7 +1064,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

@@ -54,7 +54,9 @@ public class AssertTools {
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition) {
checkCondition(condition, IllegalArgumentException::new);
if (!condition) {
throw new IllegalArgumentException();
}
}
/**
@@ -65,7 +67,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 +80,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 +95,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));
}
}
// ================================
@@ -109,7 +116,9 @@ public class AssertTools {
* @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;
}
@@ -122,7 +131,9 @@ public class AssertTools {
* @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;
}
@@ -135,7 +146,9 @@ public class AssertTools {
* @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;
}
@@ -150,8 +163,9 @@ public class AssertTools {
*/
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 +184,9 @@ public class AssertTools {
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition) {
checkCondition(condition, IllegalStateException::new);
if (!condition) {
throw new IllegalStateException();
}
}
/**
@@ -181,7 +197,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 +210,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 +225,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 +246,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 +260,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 +274,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 +290,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 +313,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 +330,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 +347,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 +366,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 +382,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 +399,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 +416,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 +435,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();
}

View File

@@ -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.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
@@ -69,20 +69,20 @@ 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);
}
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);
}
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,20 +98,20 @@ 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);
}
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);
}
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);
}

View File

@@ -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;
@@ -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));
}

View File

@@ -0,0 +1,108 @@
/*
* 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 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 ZhouXY108 <luquanlion@outlook.com>
*
* @see Deflater
* @see Inflater
*/
public class ZipTools {
/**
* 使用默认压缩级别压缩数据
*
* @param input 输入
* @param level 压缩级别
* @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 baos = new ByteArrayOutputStream();
try (DeflaterOutputStream dos = new DeflaterOutputStream(baos, new Deflater(level))) {
dos.write(input);
dos.finish();
return baos.toByteArray();
}
}
/**
* 解压数据
*
* @param input 输入
* @return 解压后的数据
*
* @throws IOException 发生 I/O 错误时抛出
*/
@CheckForNull
public static byte[] unzip(@Nullable byte[] input) throws IOException {
if (input == null) {
return null;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InflaterOutputStream dos = new InflaterOutputStream(baos, new Inflater())) {
dos.write(input);
dos.finish();
return baos.toByteArray();
}
}
private ZipTools() {
throw new IllegalStateException("Utility class");
}
}

View File

@@ -0,0 +1,55 @@
package xyz.zhouxy.plusone.commons.model;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Arrays;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SemVerTests {
@Test
void testCompareTo() {
SemVer[] versions = {
SemVer.of("1.0.0-alpha.beta"),
SemVer.of("1.0.1"),
SemVer.of("1.0.0-beta.11"),
SemVer.of("1.0.0-beta"),
SemVer.of("1.0.0-alpha"),
SemVer.of("1.0.0-beta.2"),
SemVer.of("1.0.0-rc.1"),
SemVer.of("1.0.0"),
SemVer.of("1.0.0-alpha.1"),
// SemVer.of("10.20.30.40.50-RC2.20250904"),
// SemVer.of("10.20.30.40.50-RC1.20250801"),
// SemVer.of("10.20.30.40.50-M1.2"),
// SemVer.of("10.20.30.40.50-M1"),
// SemVer.of("10.3.30-50"),
// SemVer.of("10.20.30-50"),
// SemVer.of("10.20.30-beta"),
// SemVer.of("10.20.30-alpha"),
// SemVer.of("10.20.30-2a"),
// SemVer.of("10.20.30-RC1"),
// SemVer.of("10.20.30-RC2"),
// SemVer.of("10.20.30-M2"),
// SemVer.of("10.20.30-M1"),
// SemVer.of("10.20.30-20"),
// SemVer.of("10.20.30.40-10"),
};
String compareResult = Arrays.stream(versions)
.sorted()
.map(SemVer::getValue)
.collect(Collectors.joining(" < "));
log.info(compareResult);
assertEquals(
"1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 < 1.0.1",
compareResult);
}
}

View File

@@ -0,0 +1,86 @@
package xyz.zhouxy.plusone.commons.util;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.Arrays;
import java.util.Random;
import java.util.zip.DataFormatException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import com.google.common.base.Stopwatch;
import com.google.common.io.Resources;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ZipToolsTests {
static byte[] bytes;
@BeforeAll
static void setup() throws IOException {
URL resource = Resources.getResource("xyz/zhouxy/plusone/commons/util/LoremIpsum.txt");
bytes = Resources.toByteArray(resource);
}
@Test
void zip_WithDefaultLevel() throws IOException, DataFormatException {
Stopwatch stopwatch = Stopwatch.createStarted();
byte[] zip = ZipTools.zip(bytes);
stopwatch.stop();
log.info("default level, size: {} ({})", zip.length, stopwatch);
assertArrayEquals(bytes, ZipTools.unzip(zip));
assertNull(ZipTools.zip(null));
assertNull(ZipTools.unzip(null));
}
@ParameterizedTest
@ValueSource(ints = { -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })
void zip_WithLevel(int level) throws IOException, DataFormatException {
Stopwatch stopwatch = Stopwatch.createStarted();
byte[] zip = ZipTools.zip(bytes, level);
stopwatch.stop();
log.info("level: {}, size: {} ({})", level, zip.length, stopwatch);
assertArrayEquals(bytes, ZipTools.unzip(zip));
assertNull(ZipTools.zip(null, level));
assertNull(ZipTools.unzip(null));
}
@Test
void zip_WithWrongLevel() throws IOException, DataFormatException {
Random random = new Random();
final int levelGtMax = random.nextInt() + 9;
assertThrows(IllegalArgumentException.class, () -> ZipTools.zip(bytes, levelGtMax));
final int levelLtMin = -1 - random.nextInt();
assertThrows(IllegalArgumentException.class, () -> ZipTools.zip(bytes, levelLtMin));
}
@Test
void test_constructor_isNotAccessible_ThrowsIllegalStateException() {
Constructor<?>[] constructors = ZipTools.class.getDeclaredConstructors();
Arrays.stream(constructors)
.forEach(constructor -> {
assertFalse(constructor.isAccessible());
constructor.setAccessible(true);
Throwable cause = assertThrows(Exception.class, constructor::newInstance)
.getCause();
assertInstanceOf(IllegalStateException.class, cause);
assertEquals("Utility class", cause.getMessage());
});
}
}

File diff suppressed because it is too large Load Diff