refactor: split modules to separate repos

This commit is contained in:
2026-06-02 13:42:43 +08:00
parent 7e1b7cae85
commit 80bec17c7d
49 changed files with 132 additions and 5168 deletions
+32 -29
View File
@@ -1,46 +1,49 @@
# OnixByte Toolbox # Version Catalogue
![Static Badge](https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Fcom%2Fonixbyte%2Fversion-catalogue%2Fmaven-metadata.xml&query=%2F%2Fmetadata%2Fversioning%2Flatest&label=version) The **Version Catalogue** (Bill of Materials) is a Maven POM file provided by OnixByte to manage
![Static Badge](https://img.shields.io/badge/licence-MIT-green) dependency versions for the **OnixByte Toolbox**. By incorporating this BOM into your build
![Static Badge](https://img.shields.io/badge/java-%E2%89%A517-blue) configuration, you can ensure consistent versioning across all included dependencies without
needing to specify versions explicitly in your project files. Published with Gradle metadata,
this BOM supports both Maven and Gradle projects, and this document outlines how to integrate
and use it effectively in both ecosystems.
## Using in Maven
OnixByte Toolbox is a Java Development Kit that offers a set of convenient tools for writing code efficiently. Add the `version-catalogue` to your `pom.xml` under `<dependencyManagement>`:
## Installation and Usage
If you are using **Maven**, please paste the following codes to _pom.xml_ in your project.
```xml ```xml
<dependencyManagement>
<dependencies>
<dependency> <dependency>
<groupId>com.onixbyte</groupId> <groupId>com.onixbyte</groupId>
<artifactId>${artifactId}</artifactId> <artifactId>version-catalogue</artifactId>
<version>${version}</version> <version>3.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency> </dependency>
</dependencies>
</dependencyManagement>
``` ```
If you are using **Gradle**, please paste the following codes to _buile.gradle\[.kts\]_ in your project. Then reference any dependency built by OnixByte without a version.
## Using in Gradle
In your `build.gradle[.kts]`, apply the BOM using the `platform` dependency:
```groovy ```groovy
implementation 'com.onixbyte:$artifactId:$version' dependencies {
implementation platform('com.onixbyte:version-catalogue:3.0.0')
implementation 'com.onixbyte:common-toolbox'
}
``` ```
If you are using Kotlin DSL:
```kotlin ```kotlin
implementation("com.onixbyte:$artifactId:$version") dependencies {
implementation(platform("com.onixbyte:version-catalogue:3.0.0"))
implementation("com.onixbyte:common-toolbox")
}
``` ```
If you want to check the available versions, please check out at our [official site](https://codecrafters.org.cn/devkit/changelog).
## Contribution
Contributions are welcome! If you encounter any issues or want to contribute to the project, please feel free to **[raise an issue](https://github.com/CodeCraftersCN/jdevkit/issues/new)** or **[submit a pull request](https://github.com/CodeCraftersCN/jdevkit/compare)**.
## License
This project is licensed under the [MIT](/LICENSE).
## Contact
If you have any suggestions, ideas, don't hesitate contacting us via [GitHub Issues](https://github.com/CodeCraftersCN/jdevkit/issues/new) or [Discord Community](https://discord.gg/NQK9tjcBB8).
If you face any bugs while using our library and you are able to fix any bugs in our library, we would be happy to accept pull requests from you on [GitHub](https://github.com/CodeCraftersCN/jdevkit/compare).
+96 -2
View File
@@ -20,6 +20,100 @@
* SOFTWARE. * SOFTWARE.
*/ */
subprojects { import java.net.URI
group = "com.onixbyte"
plugins {
id("java-platform")
id("maven-publish")
id("signing")
}
val commonToolboxVersion: String by project
val identityGeneratorVersion: String by project
val cryptoToolboxVersion: String by project
val mathToolboxVersion: String by project
val tupleVersion: String by project
val versionCatalogueVersion: String by project
version = versionCatalogueVersion
val projectUrl: String by project
val projectGithubUrl: String by project
val licenseName: String by project
val licenseUrl: String by project
repositories {
mavenCentral()
}
dependencies {
constraints {
api("com.onixbyte:common-toolbox:$commonToolboxVersion")
api("com.onixbyte:identity-generator:$identityGeneratorVersion")
api("com.onixbyte:crypto-toolbox:$cryptoToolboxVersion")
api("com.onixbyte:math-toolbox:$mathToolboxVersion")
api("com.onixbyte:tuple:$tupleVersion")
}
}
publishing {
publications {
create<MavenPublication>("versionCatalogue") {
groupId = group.toString()
artifactId = "version-catalogue"
version = versionCatalogueVersion
pom {
name = "OnixByte Version Catalogue"
description = "OnixByte DevKit BOM is designed to manage dependency versions centrally."
url = projectUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
scm {
connection = "scm:git:git://github.com:onixbyte/onixbyte-toolbox.git"
developerConnection = "scm:git:git://github.com:onixbyte/onixbyte-toolbox.git"
url = projectGithubUrl
}
developers {
developer {
id = "zihluwang"
name = "Zihlu Wang"
email = "really@zihlu.wang"
timezone = "Asia/Hong_Kong"
}
developer {
id = "siujamo"
name = "Siu Jam'o"
email = "jamo.siu@outlook.com"
timezone = "Asia/Shanghai"
}
}
}
from(components["javaPlatform"])
signing {
setRequired(project.hasProperty("signing.keyId"))
sign(publishing.publications["versionCatalogue"])
}
}
repositories {
maven {
name = "sonatypeNexus"
url = URI(providers.gradleProperty("repo.maven-central.host").get())
credentials {
username = providers.gradleProperty("repo.maven-central.username").get()
password = providers.gradleProperty("repo.maven-central.password").get()
}
}
}
}
} }
-17
View File
@@ -1,17 +0,0 @@
# Common Toolbox
## Introduction
Common Toolbox is a Java SE utility library, that provides a collection of utility to streamline
your Java coding experience.
## Features
- AES encryption and decryption;
- Base64 encode and decode;
- Boolean calculation;
- Reduce `if...else...` with **lambdas**;
- Hash calculation for strings;
- Convert Java beans to map and map to Java beans;
- Simplified range generator.
-131
View File
@@ -1,131 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import java.net.URI
plugins {
java
id("java-library")
id("maven-publish")
id("signing")
}
val commonToolboxVersion: String by project
version = commonToolboxVersion
val projectUrl: String by project
val projectGithubUrl: String by project
val licenseName: String by project
val licenseUrl: String by project
repositories {
mavenCentral()
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
withSourcesJar()
withJavadocJar()
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
tasks.withType<Jar> {
exclude("logback.xml")
}
dependencies {
compileOnly(libs.slf4j)
implementation(libs.logback)
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
}
tasks.test {
useJUnitPlatform()
}
publishing {
publications {
create<MavenPublication>("commonToolbox") {
groupId = group.toString()
artifactId = "common-toolbox"
version = commonToolboxVersion
pom {
name = "OnixByte Common Toolbox"
description = "The utils module of OnixByte toolbox."
url = projectUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
scm {
connection = "scm:git:git://github.com:onixbyte/onixbyte-toolbox.git"
developerConnection = "scm:git:git://github.com:onixbyte/onixbyte-toolbox.git"
url = projectGithubUrl
}
developers {
developer {
id = "zihluwang"
name = "Zihlu Wang"
email = "really@zihlu.wang"
timezone = "Asia/Hong_Kong"
}
developer {
id = "siujamo"
name = "Siu Jam'o"
email = "jamo.siu@outlook.com"
timezone = "Asia/Shanghai"
}
}
}
from(components["java"])
signing {
setRequired(project.hasProperty("signing.keyId"))
sign(publishing.publications["commonToolbox"])
}
}
repositories {
maven {
name = "sonatypeNexus"
url = URI(providers.gradleProperty("repo.maven-central.host").get())
credentials {
username = providers.gradleProperty("repo.maven-central.username").get()
password = providers.gradleProperty("repo.maven-central.password").get()
}
}
}
}
}
@@ -1,85 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.common.adapter;
import java.util.Map;
/**
* The {@link ObjectMapAdapter} interface provides methods to convert between objects and maps.
* This interface is useful for scenarios where objects need to be represented as maps for
* serialization, deserialization, or other purposes.
*
* <p>Implementations of this interface should provide the logic to convert an object of type
* {@code T} to a {@link Map} and vice versa.</p>
*
* <p><b>Example usage:</b></p>
* <pre>{@code
* public class User {
* private String name;
* private int age;
*
* // getters and setters
* }
*
* public class UserMapAdapter implements ObjectMapAdapter<User> {
* @Override
* public Map<String, Object> toMap(User user) {
* Map<String, Object> map = new HashMap<>();
* map.put("name", user.getName());
* map.put("age", user.getAge());
* return map;
* }
*
* @Override
* public User fromMap(Map<String, Object> map) {
* User user = new User();
* user.setName((String) map.get("name"));
* user.setAge((Integer) map.get("age"));
* return user;
* }
* }
* }</pre>
*
* @param <T> the type of the object to be converted
* @author zihluwang
* @version 3.0.0
*/
public interface ObjectMapAdapter<T> {
/**
* Convert an object to a map.
*
* @param element the element that will be converted to Map
* @return a Map that is converted from the element
*/
Map<String, Object> toMap(T element);
/**
* Convert a Map to an object.
*
* @param map the map that will be converted to an object
* @return the object that is converted from the Map
*/
T toObject(Map<String, Object> map);
}
@@ -1,206 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.common.util;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.Base64;
import java.util.UUID;
/**
* The {@link AesUtil} class provides utility methods for encrypting and decrypting data using the
* AES algorithm. This class supports both byte array and string data, and uses a specified secret
* key for encryption and decryption.
* <p>
* The utility methods in this class are useful for scenarios where data needs to be securely
* encrypted and decrypted.
* </p>
*
* <p><b>Example usage:</b></p>
* <pre>{@code
* // Encrypting and decrypting byte array data
* byte[] secretKey = "43f72073956d4c81".getBytes(StandardCharsets.UTF_8);
* byte[] data = "Hello World".getBytes(StandardCharsets.UTF_8);
* byte[] encryptedData = AesUtil.encrypt(data, secretKey);
* byte[] decryptedData = AesUtil.decrypt(encryptedData, secretKey);
* System.out.println(new String(decryptedData, StandardCharsets.UTF_8)); // Output: Hello World
*
* // Encrypting and decrypting string data
* String secret = "43f72073956d4c81";
* String encryptedString = AesUtil.encrypt("Hello World", secret);
* String decryptedString = AesUtil.decrypt(encryptedString, secret);
* System.out.println(decryptedString); // Output: Hello World
*
* // Generating a random secret key
* String randomSecret = AesUtil.generateRandomSecret();
* System.out.println(randomSecret); // Output: A randomly generated 16-character long secret
* }</pre>
*
* @author hubin
* @version 3.0.0
*/
public final class AesUtil {
/**
* The algorithm AES.
*/
private static final String AES = "AES";
/**
* The algorithm AES/CBC/PKCS5Padding.
*/
private static final String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding";
/**
* Private constructor to prevent instantiation of this utility class.
*/
private AesUtil() {
}
/**
* Encrypts the specified data using the AES algorithm with the provided secret key.
*
* @param data the data to be encrypted
* @param secret the secret key used for encryption
* @param ivParam the iv param
* @return the encrypted data as a byte array
* @throws GeneralSecurityException if any cryptographic error occurs during encryption
*/
public static byte[] encrypt(byte[] data, byte[] secret, byte[] ivParam) throws GeneralSecurityException {
var secretKeySpec = new SecretKeySpec(new SecretKeySpec(secret, AES).getEncoded(), AES);
var cipher = Cipher.getInstance(AES_CBC_CIPHER);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(ivParam));
return cipher.doFinal(data);
}
/**
* Encrypts the specified data using the AES algorithm with the provided secret key.
*
* @param data the data to be encrypted
* @param secret the secret key used for encryption
* @return the encrypted data as a byte array
* @throws GeneralSecurityException if any cryptographic error occurs during encryption
*/
public static byte[] encrypt(byte[] data, byte[] secret) throws GeneralSecurityException {
return encrypt(data, secret, secret);
}
/**
* Decrypts the specified data using the AES algorithm with the provided secret key.
*
* @param data the data to be decrypted
* @param secret the secret key used for decryption
* @param ivParam the iv param
* @return the decrypted data as a byte array
* @throws GeneralSecurityException if any cryptographic error occurs during decryption
*/
public static byte[] decrypt(byte[] data, byte[] secret, byte[] ivParam) throws GeneralSecurityException {
var secretKeySpec = new SecretKeySpec(new SecretKeySpec(secret, AES).getEncoded(), AES);
var cipher = Cipher.getInstance(AES_CBC_CIPHER);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(ivParam));
return cipher.doFinal(data);
}
/**
* Decrypts the specified data using the AES algorithm with the provided secret key.
*
* @param data the data to be decrypted
* @param secret the secret key used for decryption
* @return the decrypted data as a byte array
* @throws GeneralSecurityException if any cryptographic error occurs during decryption
*/
public static byte[] decrypt(byte[] data, byte[] secret) throws GeneralSecurityException {
return decrypt(data, secret, secret);
}
/**
* Encrypts the specified string data using the AES algorithm with the provided secret key.
*
* @param data the string data to be encrypted
* @param secret the secret key used for encryption
* @param ivParam the iv param
* @return the encrypted data encoded in Base64
* @throws GeneralSecurityException if any cryptographic error occurs during encryption
*/
public static String encrypt(String data, String secret, String ivParam) throws GeneralSecurityException {
return Base64.getEncoder().encodeToString(encrypt(
data.getBytes(StandardCharsets.UTF_8),
secret.getBytes(StandardCharsets.UTF_8),
ivParam.getBytes(StandardCharsets.UTF_8)
));
}
/**
* Encrypts the specified string data using the AES algorithm with the provided secret key.
*
* @param data the string data to be encrypted
* @param secret the secret key used for encryption
* @return the encrypted data encoded in Base64
* @throws GeneralSecurityException if any cryptographic error occurs during encryption
*/
public static String encrypt(String data, String secret) throws GeneralSecurityException {
return encrypt(data, secret, secret);
}
/**
* Decrypts the specified Base64-encoded string data using the AES algorithm with the provided secret key.
*
* @param data the Base64-encoded string data to be decrypted
* @param secret the secret key used for decryption
* @param ivParam the initialization vector parameter used for AES decryption
* @return the decrypted string data
* @throws GeneralSecurityException if any cryptographic error occurs during decryption
*/
public static String decrypt(String data, String secret, String ivParam) throws GeneralSecurityException {
var decrypted = decrypt(
Base64.getDecoder().decode(data.getBytes(StandardCharsets.UTF_8)),
secret.getBytes(StandardCharsets.UTF_8),
ivParam.getBytes(StandardCharsets.UTF_8)
);
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* Decrypts the specified Base64-encoded string data using the AES algorithm with the provided secret key.
*
* @param data the Base64-encoded string data to be decrypted
* @param secret the secret key used for decryption
* @return the decrypted string data
* @throws GeneralSecurityException if any cryptographic error occurs during decryption
*/
public static String decrypt(String data, String secret) throws GeneralSecurityException {
return decrypt(data, secret, secret);
}
/**
* Generates 16-character random secret.
*
* @return the generated secure secret
*/
public static String generateRandomSecret() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
}
}
@@ -1,212 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.common.util;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
/**
* The {@link Base64Util} class provides static methods to encode and decode strings with Base64
* encoding. It utilizes the {@link Base64} class from the Java standard library for performing the
* encoding and decoding operations. This utility class offers convenient methods to encode and
* decode strings with different character sets.
* <p>
* This class is designed as a final class with a private constructor to prevent instantiation.
* All methods in this class are static, allowing easy access to the Base64 encoding and
* decoding functionality.
* <p>
* Example usage:
* <pre>
* String original = "Hello, World!";
*
* // Encode the string using UTF-8 charset
* String encoded = Base64Util.encode(original);
* System.out.println("Encoded string: " + encoded);
*
* // Decode the encoded string using UTF-8 charset
* String decoded = Base64Util.decode(encoded);
* System.out.println("Decoded string: " + decoded);
* </pre>
* <p>
* <b>Note:</b> This utility class uses the default charset (UTF-8) if no specific charset is
* provided. It is recommended to specify the charset explicitly to ensure consistent
* encoding and decoding.
*
* @author zihluwang
* @version 3.0.0
*/
public final class Base64Util {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private Base64Util() {
}
private static Base64.Encoder encoder;
private static Base64.Decoder decoder;
private static Base64.Encoder urlEncoder;
private static Base64.Decoder urlDecoder;
/**
* Ensure that there is only one Base64 Encoder.
*
* @return the {@link Base64.Encoder} instance
*/
private static Base64.Encoder getEncoder() {
if (Objects.isNull(encoder)) {
encoder = Base64.getEncoder();
}
return encoder;
}
/**
* Ensure that there is only one Base64 Encoder.
*
* @return the {@link Base64.Encoder} instance
*/
private static Base64.Decoder getDecoder() {
if (Objects.isNull(decoder)) {
decoder = Base64.getDecoder();
}
return decoder;
}
/**
* Ensure that there is only one Base64 URL Encoder.
*
* @return the {@link Base64.Encoder} instance
*/
private static Base64.Encoder getUrlEncoder() {
if (Objects.isNull(urlEncoder)) {
urlEncoder = Base64.getUrlEncoder();
}
return urlEncoder;
}
/**
* Ensure that there is only one Base64 URL Decoder.
*
* @return the {@link Base64.Encoder} instance
*/
public static Base64.Decoder getUrlDecoder() {
if (Objects.isNull(urlDecoder)) {
urlDecoder = Base64.getUrlDecoder();
}
return urlDecoder;
}
/**
* Encodes the given string using the specified charset.
*
* @param value the string to be encoded
* @param charset the charset to be used for encoding
* @return the Base64 encoded string
*/
public static String encode(String value, Charset charset) {
var encoded = getEncoder().encode(value.getBytes(charset));
return new String(encoded);
}
/**
* Encodes the given string using the default UTF-8 charset.
*
* @param value the string to be encoded
* @return the Base64 encoded string
*/
public static String encode(String value) {
return encode(value, StandardCharsets.UTF_8);
}
/**
* Decodes the given Base64 encoded string using the specified charset.
*
* @param value the Base64 encoded string to be decoded
* @param charset the charset to be used for decoding
* @return the decoded string
*/
public static String decode(String value, Charset charset) {
var decoded = getDecoder().decode(value.getBytes(charset));
return new String(decoded);
}
/**
* Decodes the given Base64 encoded string using the default UTF-8 charset.
*
* @param value the Base64 encoded string to be decoded
* @return the decoded string
*/
public static String decode(String value) {
return decode(value, StandardCharsets.UTF_8);
}
/**
* Encodes the given string using the specified charset.
*
* @param value the string to be encoded
* @param charset the charset to be used for encoding
* @return the Base64 encoded string
*/
public static String encodeUrlComponents(String value, Charset charset) {
var encoded = getUrlEncoder().encode(value.getBytes(charset));
return new String(encoded);
}
/**
* Encodes the given string using the default UTF-8 charset.
*
* @param value the string to be encoded
* @return the Base64 encoded string
*/
public static String encodeUrlComponents(String value) {
return encodeUrlComponents(value, StandardCharsets.UTF_8);
}
/**
* Decodes the given Base64 encoded string using the specified charset.
*
* @param value the Base64 encoded string to be decoded
* @param charset the charset to be used for decoding
* @return the decoded string
*/
public static String decodeUrlComponents(String value, Charset charset) {
var decoded = getUrlDecoder().decode(value.getBytes(charset));
return new String(decoded);
}
/**
* Decodes the given Base64 encoded string using the default UTF-8 charset.
*
* @param value the Base64 encoded string to be decoded
* @return the decoded string
*/
public static String decodeUrlComponents(String value) {
return decodeUrlComponents(value, StandardCharsets.UTF_8);
}
}
@@ -1,102 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.common.util;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.BooleanSupplier;
/**
* The {@link BoolUtil} class provides utility methods for boolean calculations.
* This class offers methods to perform logical operations such as AND, OR, and NOT on boolean values.
* <p>
* The utility methods in this class are useful for scenarios where multiple boolean values need to be
* evaluated together, and for simplifying complex boolean expressions.
* </p>
*
* <p><b>Example usage:</b></p>
* <pre>{@code
* boolean result1 = BoolUtil.and(true, true, false); // false
* boolean result2 = BoolUtil.or(true, false, false); // true
* boolean result3 = BoolUtil.not(false); // true
* }</pre>
*
* @author zihluwang
* @version 3.0.0
*/
public final class BoolUtil {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private BoolUtil() {
}
/**
* Logical and calculation.
*
* @param values the values to be calculated
* @return {@code true} if all value in values is {@code true}, otherwise {@code false}
*/
public static boolean and(Boolean... values) {
return Arrays.stream(values)
.filter(Objects::nonNull)
.allMatch(Boolean::booleanValue);
}
/**
* Logical and calculation.
*
* @param valueSuppliers the suppliers of value to be calculated
* @return {@code true} if all value in values is {@code true}, otherwise {@code false}
*/
public static boolean and(BooleanSupplier... valueSuppliers) {
return Arrays.stream(valueSuppliers)
.filter(Objects::nonNull)
.allMatch(BooleanSupplier::getAsBoolean);
}
/**
* Logical or calculation.
*
* @param values the values to be calculated
* @return {@code true} if any value in values is {@code true}, otherwise {@code false}
*/
public static boolean or(Boolean... values) {
return Arrays.stream(values)
.filter(Objects::nonNull)
.anyMatch(Boolean::booleanValue);
}
/**
* Logical or calculation.
*
* @param valueSuppliers the suppliers of value to be calculated
* @return {@code true} if any value in values is {@code true}, otherwise {@code false}
*/
public static boolean or(BooleanSupplier... valueSuppliers) {
return Arrays.stream(valueSuppliers)
.filter(Objects::nonNull)
.anyMatch(BooleanSupplier::getAsBoolean);
}
}
@@ -1,213 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.common.util;
import java.util.Objects;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
/**
* The {@link BranchUtil} class provides static methods to simplify conditional logic in Java
* development by leveraging lambda expressions. It offers convenient methods to replace verbose
* {@code if...else} statements with more concise and expressive functional constructs.
* <p>
* Developers can use methods in this utility class to streamline their code, enhance readability,
* and promote a more functional style of programming when dealing with branching logic and
* conditional statements.
* <p>
* <b>Example:</b>
* <pre>
* // If you want to simplify an if (exp1 || exp2), you can use the following code:
* String r1 = BranchUtil.or(1 == 1, 2 == 1)
* .handle(() -> "1 is equal to 1 or 2 is equal to 1.");
*
* // If you have an else branch, you can use the following code:
* String r2 = BranchUtil.or(1 == 1, 2 == 1)
* .handle(() -> "1 is equal to 1 or 2 is equal to 1.",
* () -> "1 is not equal to 1 and 2 is not equal to 1.");
*
* // If you only need to execute code without a return value:
* BranchUtil.or(1 == 1, 2 == 1)
* .handle(() -> {
* // do something
* }, () -> {
* // do something
* });
* // If you only need an if branch, you can remove the second Supplier instance.
*
* // To check if all boolean expressions are true, use the 'and' method:
* BranchUtil.and(1 == 1, 2 == 1)
* .handle(() -> {
* // do something
* }, () -> {
* // do something
* });
* </pre>
* <p>
* <b>Note:</b>
* The {@link #and(Boolean...)} and {@link #or(Boolean...)} methods accept any number of boolean
* expressions.
*
* @author zihluwang
* @version 3.0.0
* @see java.util.function.Supplier
* @see java.util.function.BooleanSupplier
* @see java.lang.Runnable
*/
public final class BranchUtil {
/**
* The final result of the boolean expression.
*/
private final boolean result;
/**
* Create a {@code BranchUtil} instance.
*
* @param result the result of the boolean expressions.
*/
private BranchUtil(boolean result) {
this.result = result;
}
/**
* Creates a {@code BranchUtil} instance to evaluate a logical OR operation on the provided
* boolean expressions.
*
* @param values the boolean expressions to be evaluated
* @return a {@code BranchUtil} instance representing the result of the logical OR operation
*/
public static BranchUtil or(Boolean... values) {
return new BranchUtil(BoolUtil.or(values));
}
/**
* Creates a {@code BranchUtil} instance to evaluate a logical AND operation on the provided
* boolean expressions.
*
* @param values the boolean expressions to be evaluated
* @return a {@code BranchUtil} instance representing the result of the logical AND operation
*/
public static BranchUtil and(Boolean... values) {
return new BranchUtil(BoolUtil.and(values));
}
/**
* Creates a {@code BranchUtil} instance to evaluate a logical OR operation on the provided
* boolean suppliers.
*
* @param valueSuppliers the boolean suppliers to be evaluated
* @return a {@code BranchUtil} instance representing the result of the
* logical OR operation
*/
public static BranchUtil or(BooleanSupplier... valueSuppliers) {
return new BranchUtil(BoolUtil.or(valueSuppliers));
}
/**
* Creates a {@code BranchUtil} instance to evaluate a logical AND operation on the provided
* boolean suppliers.
*
* @param valueSuppliers the boolean suppliers to be evaluated
* @return a {@code BranchUtil} instance representing the result of the
* logical AND operation
*/
public static BranchUtil and(BooleanSupplier... valueSuppliers) {
return new BranchUtil(BoolUtil.and(valueSuppliers));
}
/**
* Handles the result of the boolean expressions by executing the appropriate handler based
* on the result.
* <p>
* If the result is {@code true}, the {@code trueSupplier} is executed. If the result is
* {@code false} and an {@code falseSupplier} is provided, it is executed.
* <p>
* Returns the result of the executed supplier.
*
* @param <T> the type of the result to be handled by the methods
* @param trueSupplier the supplier to be executed if the result is {@code true}
* @param falseSupplier the supplier to be executed if the result is {@code false} (optional)
* @return the result of the executed supplier, or {@code null} if no {@code falseSupplier} is
* provided and the result of the evaluation is {@code false}
*/
public <T> T thenSupply(Supplier<T> trueSupplier, Supplier<T> falseSupplier) {
if (this.result && Objects.nonNull(trueSupplier)) {
return trueSupplier.get();
}
if (Objects.isNull(falseSupplier)) {
return null;
}
return falseSupplier.get();
}
/**
* Handles the result of the boolean expressions by executing the provided handler if the
* result is {@code true}.
* <p>
* Returns the result of the executed handler.
*
* @param <T> the type of the result to be handled by the methods
* @param trueSupplier the supplier to be executed if the result is {@code true}
* @return the result of the executed handler, or {@code null} if result of evaluation
* is {@code false}
*/
public <T> T thenSupply(Supplier<T> trueSupplier) {
return thenSupply(trueSupplier, null);
}
/**
* Handles the result of the boolean expressions by executing the appropriate handler based
* on the result.
* <p>
* If the result is {@code true}, the {@code ifHandler} is executed. If the result is
* {@code false} and an {@code elseHandler} is provided, it is executed.
*
* @param trueHandler the handler to be executed if the result is {@code true}
* @param falseHandler the handler to be executed if the result is {@code false} (optional)
*/
public void then(Runnable trueHandler, Runnable falseHandler) {
if (this.result && Objects.nonNull(trueHandler)) {
trueHandler.run();
return;
}
if (Objects.isNull(falseHandler)) {
return;
}
falseHandler.run();
}
/**
* Handles the result of the boolean expressions by executing the provided handler if the
* result is {@code true}.
*
* @param trueHandler the handler to be executed if the result is {@code true}
*/
public void then(Runnable trueHandler) {
then(trueHandler, null);
}
}
@@ -1,135 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.common.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
/**
* A utility class providing static methods for manipulating collections.
*
* @author zihluwang
* @version 3.0.0
*/
public final class CollectionUtil {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private CollectionUtil() {
}
/**
* Splits a collection into a list of sub-collections, each with a maximum size specified by
* the caller.
* <p>
* This method takes an original collection and divides it into smaller sub-collections,
* ensuring that each sub-collection contains no more than the specified maximum size. If the
* original collection's size is less than or equal to the maximum size, it is returned as a
* single sub-collection. The sub-collections are created using the provided collection factory.
*
* @param <T> the type of elements in the collection
* @param <C> the type of the collection, which must extend {@link Collection}
* @param originalCollection the collection to be split into sub-collections
* @param maxSize the maximum number of elements allowed in each sub-collection
* @param collectionFactory a supplier that creates new instances of the sub-collection type
* @return a list of sub-collections, each containing up to {@code maxSize} elements
* @throws IllegalArgumentException if {@code originalCollection} is {@code null},
* {@code maxSize} is less than zero, or
* {@code collectionFactory} is {@code null}
*/
public static <T, C extends Collection<T>> List<C> chunk(
C originalCollection,
int maxSize,
Supplier<C> collectionFactory
) {
// check inputs
if (Objects.isNull(originalCollection)) {
throw new IllegalArgumentException("Collection must not be null.");
}
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize must greater than 0.");
}
if (Objects.isNull(collectionFactory)) {
throw new IllegalArgumentException("Factory method cannot be null.");
}
var result = new ArrayList<C>();
var size = originalCollection.size();
// if original collection is empty or the size less than maxSize, return it as a single
// sub collection
if (size <= maxSize) {
var singleCollection = collectionFactory.get();
singleCollection.addAll(originalCollection);
result.add(singleCollection);
return result;
}
// use iterator to split the given collection
var iter = originalCollection.iterator();
var count = 0;
var currentSubCollection = collectionFactory.get();
while (iter.hasNext()) {
var element = iter.next();
currentSubCollection.add(element);
count++;
// add sub collection to result when current sub collection reached maxSize or
// collection traverse is completed
if (count % maxSize == 0 || !iter.hasNext()) {
result.add(currentSubCollection);
currentSubCollection = collectionFactory.get();
}
}
return result;
}
/**
* Check if a collection is not null and not empty.
*
* @param collection the collection to check
* @return {@code true} if the collection is not null and not empty, {@code false} otherwise
*/
public static boolean notEmpty(Collection<?> collection) {
return Objects.nonNull(collection) && !collection.isEmpty();
}
/**
* Check if a collection is null or empty.
*
* @param collection the collection to check
* @return {@code true} if the collection is null or empty, {@code false} otherwise
*/
public static boolean isEmpty(Collection<?> collection) {
return Objects.isNull(collection) || collection.isEmpty();
}
}
@@ -1,121 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.common.util;
import com.onixbyte.common.adapter.ObjectMapAdapter;
import java.util.Map;
/**
* The {@link MapUtil} class provides utility methods for converting between objects and maps.
* This class leverages the {@link ObjectMapAdapter} interface to perform the conversions.
* <p>
* The utility methods in this class are useful for scenarios where objects need to be represented
* as maps for serialization, deserialization, or other purposes.
* </p>
*
* <p><b>Example usage:</b></p>
* <pre>{@code
* // User.java
* public class User {
* private String name;
* private int age;
*
* // getters and setters
* }
*
* // UserMapAdapter.java
* public class UserMapAdapter implements ObjectMapAdapter<User> {
* @Override
* public Map<String, Object> toMap(User user) {
* Map<String, Object> map = new HashMap<>();
* map.put("name", user.getName());
* map.put("age", user.getAge());
* return map;
* }
*
* @Override
* public User fromMap(Map<String, Object> map) {
* User user = new User();
* user.setName((String) map.get("name"));
* user.setAge((Integer) map.get("age"));
* return user;
* }
* }
*
* public class Example {
* public static void main(String[] args) {
* User user = new User();
* user.setName("John");
* user.setAge(30);
*
* UserMapAdapter adapter = new UserMapAdapter();
*
* // Convert object to map
* Map<String, Object> userMap = MapUtil.objectToMap(user, adapter);
* System.out.println(userMap); // Output: {name=John, age=30}
*
* // Convert map to object
* User newUser = MapUtil.mapToObject(userMap, adapter);
* System.out.println(newUser.getName()); // Output: John
* System.out.println(newUser.getAge()); // Output: 30
* }
* }
* }</pre>
*
* @author zihluwang
* @version 3.0.0
*/
public final class MapUtil {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private MapUtil() {
}
/**
* Converts an object to a map by mapping the field names to their corresponding values.
*
* @param <T> the type of the object
* @param entity the object to be converted to a map
* @param adapter adapts the entity for mapping to a map
* @return a map representing the fields and their values of the object
*/
public static <T> Map<String, Object> objectToMap(T entity, ObjectMapAdapter<T> adapter) {
return adapter.toMap(entity);
}
/**
* Converts a map to an object of the specified type by setting the field values using the
* map entries.
*
* @param objectMap the map representing the fields and their values
* @param adapter the adapter to execute the setter for the entity
* @param <T> the type of the object to be created
* @return an object of the specified type with the field values set from the map
*/
public static <T> T mapToObject(Map<String, Object> objectMap, ObjectMapAdapter<T> adapter) {
return adapter.toObject(objectMap);
}
}
@@ -1,207 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.common.util;
import java.util.stream.IntStream;
/**
* {@code RangeUtil} is a utility class providing methods for generating streams of integers that
* emulate the behaviour of Python's {@code range} function.
* <p>
* This class offers static methods to create ranges with various configurations. These methods
* leverage the {@link IntStream} to provide efficient and versatile integer sequences.
*
* @author zihluwang
* @version 3.0.0
* @see IntStream
*/
public final class RangeUtil {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private RangeUtil() {
}
/**
* Generates a stream of integers starting from {@code 0} up to the specified {@code end} value.
* <p>
* It creates a sequential, ordered {@code IntStream} that can be used for iteration or
* further processing.
* <p>
* <b>Example Usage:</b>
* <pre>{@code
* RangeUtil.range(5).forEach(System.out::println);
*
* // Output:
* // 0
* // 1
* // 2
* // 3
* // 4
* }</pre>
*
* @param end upper-bound of the range (exclusive)
* @return an {@code IntStream} of integers from {@code 0} (inclusive) to
* {@code end} (exclusive)
* @throws IllegalArgumentException if the given {@code end} value is less equal to 0
* @see IntStream
*/
public static IntStream range(int end) {
if (end <= 0) {
throw new IllegalArgumentException("Parameter [end] should not be less than or equal to 0, provided: " +
end);
}
return IntStream.range(0, end);
}
/**
* Generates a stream of integers starting from the specified {@code start} value up to the
* specified {@code end} value.
* <p>
* It creates a sequential, ordered {@code IntStream} that can be used for iteration or
* further processing.
* <p>
* If {@code start} is less than {@code end}, an ascending range (exclusive of {@code end})
* is generated. If {@code start} is greater than {@code end}, a descending range (exclusive
* of {@code end}) is generated. If {@code start} equals {@code end}, an empty stream
* is returned.
* <p>
* <b>Example Usage:</b>
* <pre>{@code
* RangeUtil.range(3, 8).forEach(System.out::println);
*
* // Output:
* // 3
* // 4
* // 5
* // 6
* // 7
*
* RangeUtil.range(8, 3).forEach(System.out::println);
*
* // Output:
* // 8
* // 7
* // 6
* // 5
* // 4
* }</pre>
*
* @param start the starting value of the range (inclusive)
* @param end upper-bound of the range (exclusive)
* @return an {@code IntStream} of integers in ascending or descending order, exclusive
* of {@code end}
* @see IntStream
*/
public static IntStream range(int start, int end) {
if (start == end) {
return IntStream.empty();
}
if (start < end) {
return IntStream.range(start, end);
} else {
// Descending range (exclusive of end)
return IntStream.iterate(start, (n) -> n > end, (n) -> n - 1);
}
}
/**
* Generates a stream of integers starting from the specified {@code start} value up to the
* specified {@code end} value.
* <p>
* It creates a sequential, ordered {@code IntStream} that can be used for iteration or
* further processing.
* <p>
* The range includes both {@code start} and {@code end}.
* <p>
* <b>Example Usage:</b>
* <pre>{@code
* RangeUtil.rangeClosed(3, 8).forEach(System.out::println);
*
* // Output:
* // 3
* // 4
* // 5
* // 6
* // 7
* // 8
* }</pre>
*
* @param start the starting value of the range (inclusive)
* @param end upper-bound of the range (inclusive)
* @return an {@code IntStream} of integers from {@code start} to {@code end} inclusive
* @see IntStream
*/
public static IntStream rangeClosed(int start, int end) {
return IntStream.rangeClosed(start, end);
}
/**
* Generates a stream of integers starting from the specified {@code start} value, incremented
* by the specified {@code step}, up to the specified {@code end} value.
* <p>
* It creates a sequential, ordered {@code IntStream} that can be used for iteration or
* further processing.
* <p>
* The stream excludes the {@code end} value.
* <p>
* <b>Example Usage:</b>
* <pre>{@code
* RangeUtil.range(3, 10, 2).forEach(System.out::println);
*
* // Output:
* // 3
* // 5
* // 7
* // 9
*
* RangeUtil.range(10, 3, -2).forEach(System.out::println);
*
* // Output:
* // 10
* // 8
* // 6
* // 4
* }</pre>
*
* @param start the starting value of the range (inclusive)
* @param end upper-bound of the range (exclusive)
* @param step the increment or decrement between each value (non-zero)
* @return an {@code IntStream} of integers from {@code start} to {@code end} exclusive stepping
* by {@code step}
* @throws IllegalArgumentException if {@code step} is zero or if {@code start} and {@code end}
* are inconsistent with the direction imposed by {@code step}
* @see IntStream
*/
public static IntStream range(int start, int end, int step) {
if (step == 0) {
throw new IllegalArgumentException("Step value must not be zero.");
}
if ((step > 0 && start >= end) || (step < 0 && start <= end)) {
throw new IllegalArgumentException("Range parameters are inconsistent with the step value.");
}
return IntStream.iterate(start, (n) -> step > 0 ? n < end : n > end, (n) -> n + step);
}
}
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2024-2026 OnixByte
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<configuration>
<property name="COLOURFUL_OUTPUT"
value="%black(%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK}) %highlight(%-5level) %black(---) %black([%10.10t]) %cyan(%-20.20logger{20}) %black(:) %msg%n"/>
<property name="STANDARD_OUTPUT"
value="%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK} %-5level %black(---) [%10.10t] %-20.20logger{20} : %msg%n"/>
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${COLOURFUL_OUTPUT}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
@@ -1,79 +0,0 @@
/*
* Copyright (C) 2024-2025 OnixByte.
*
* 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.
*/
package com.onixbyte.common.util;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import static org.junit.jupiter.api.Assertions.*;
class AesUtilTest {
@Test
void testEncryptAndDecryptByte() throws GeneralSecurityException {
byte[] secretKey = "43f72073956d4c81".getBytes(StandardCharsets.UTF_8);
byte[] originalData = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] encryptedData = AesUtil.encrypt(originalData, secretKey);
assertNotNull(encryptedData);
byte[] decryptedData = AesUtil.decrypt(encryptedData, secretKey);
assertNotNull(decryptedData);
assertArrayEquals(originalData, decryptedData);
}
@Test
void testEncryptAndDecryptString() throws GeneralSecurityException {
var secret = "43f72073956d4c81";
var originalData = "Hello World";
var encryptedData = AesUtil.encrypt(originalData, secret);
assertNotNull(encryptedData);
assertNotEquals(originalData, encryptedData);
var decryptedData = AesUtil.decrypt(encryptedData, secret);
assertNotNull(decryptedData);
assertEquals(originalData, decryptedData);
}
@Test
void testEncryptWithWrongKeyFails() throws GeneralSecurityException {
var secret = "43f72073956d4c81";
var wrongSecret = "0000000000000000";
var originalData = "Hello World";
var encryptedData = AesUtil.encrypt(originalData.getBytes(StandardCharsets.UTF_8),
secret.getBytes(StandardCharsets.UTF_8));
assertNotNull(encryptedData);
// When decrypting with the wrong key, a BadPaddingException or IllegalBlockSizeException is expected to be thrown
assertThrows(GeneralSecurityException.class, () -> {
AesUtil.decrypt(encryptedData, wrongSecret.getBytes(StandardCharsets.UTF_8));
});
}
@Test
void testGenerateRandomSecret() {
var randomSecret = AesUtil.generateRandomSecret();
assertNotNull(randomSecret);
assertEquals(16, randomSecret.length());
}
}
@@ -1,100 +0,0 @@
/*
* Copyright (C) 2024-2025 OnixByte.
*
* 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.
*/
package com.onixbyte.common.util;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
public class Base64UtilTest {
@Test
void testEncodeAndDecodeWithUtf8() {
var original = "Hello, Base64!";
var encoded = Base64Util.encode(original);
assertNotNull(encoded);
assertNotEquals(original, encoded);
var decoded = Base64Util.decode(encoded);
assertNotNull(decoded);
assertEquals(original, decoded);
}
@Test
void testEncodeAndDecodeWithCharset() {
var original = "编码测试"; // Some unicode characters (Chinese)
var charset = StandardCharsets.UTF_8;
var encoded = Base64Util.encode(original, charset);
assertNotNull(encoded);
assertNotEquals(original, encoded);
var decoded = Base64Util.decode(encoded, charset);
assertNotNull(decoded);
assertEquals(original, decoded);
}
@Test
void testEncodeUrlComponentsAndDecodeWithUtf8() {
var original = "This is a test for URL-safe Base64 encoding+!";
var encodedUrl = Base64Util.encodeUrlComponents(original);
assertNotNull(encodedUrl);
assertNotEquals(original, encodedUrl);
// URL-safe encoding should not contain '+' or '/' characters
assertFalse(encodedUrl.contains("+"));
assertFalse(encodedUrl.contains("/"));
var decodedUrl = Base64Util.decodeUrlComponents(encodedUrl);
assertNotNull(decodedUrl);
assertEquals(original, decodedUrl);
}
@Test
void testEncodeUrlComponentsAndDecodeWithCharset() {
var original = "测试 URL 安全编码"; // Unicode string
var charset = StandardCharsets.UTF_8;
var encodedUrl = Base64Util.encodeUrlComponents(original, charset);
assertNotNull(encodedUrl);
assertNotEquals(original, encodedUrl);
var decodedUrl = Base64Util.decodeUrlComponents(encodedUrl, charset);
assertNotNull(decodedUrl);
assertEquals(original, decodedUrl);
}
@Test
void testEncodeAndDecodeEmptyString() {
var original = "";
var encoded = Base64Util.encode(original);
assertNotNull(encoded);
assertEquals("", Base64Util.decode(encoded));
}
@Test
void testEncodeAndDecodeNullSafety() {
// Since Base64Util does not explicitly handle null, the test expects NPE if null is input
assertThrows(NullPointerException.class, () -> Base64Util.encode(null));
assertThrows(NullPointerException.class, () -> Base64Util.decode(null));
}
}
@@ -1,137 +0,0 @@
/*
* Copyright (C) 2024-2025 OnixByte.
*
* 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.
*/
package com.onixbyte.common.util;
import org.junit.jupiter.api.Test;
import java.util.function.BooleanSupplier;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class BoolUtilTest {
// Tests for and(Boolean... values)
@Test
void and_AllTrueValues_ReturnsTrue() {
assertTrue(BoolUtil.and(true, true, true));
}
@Test
void and_SomeFalseValues_ReturnsFalse() {
assertFalse(BoolUtil.and(true, false, true));
}
@Test
void and_AllFalseValues_ReturnsFalse() {
assertFalse(BoolUtil.and(false, false));
}
@Test
void and_WithNullValues_IgnoresNulls() {
assertTrue(BoolUtil.and(true, null, true));
assertFalse(BoolUtil.and(true, null, false));
}
@Test
void and_AllNullValues_ReturnsTrue() {
// Stream after filtering null is empty, allMatch on empty returns true
assertTrue(BoolUtil.and((Boolean) null, null));
}
// Tests for and(BooleanSupplier... valueSuppliers)
@Test
void and_AllSuppliersTrue_ReturnsTrue() {
BooleanSupplier trueSupplier = () -> true;
BooleanSupplier falseSupplier = () -> false;
assertTrue(BoolUtil.and(trueSupplier, trueSupplier));
assertFalse(BoolUtil.and(trueSupplier, falseSupplier));
}
@Test
void and_WithNullSuppliers_IgnoresNull() {
BooleanSupplier trueSupplier = () -> true;
assertTrue(BoolUtil.and(trueSupplier, null, trueSupplier));
assertFalse(BoolUtil.and(trueSupplier, null, () -> false));
}
@Test
void and_AllNullSuppliers_ReturnsTrue() {
assertTrue(BoolUtil.and((BooleanSupplier) null, null));
}
// Tests for or(Boolean... values)
@Test
void or_AllTrueValues_ReturnsTrue() {
assertTrue(BoolUtil.or(true, true, true));
}
@Test
void or_SomeTrueValues_ReturnsTrue() {
assertTrue(BoolUtil.or(false, true, false));
}
@Test
void or_AllFalseValues_ReturnsFalse() {
assertFalse(BoolUtil.or(false, false));
}
@Test
void or_WithNullValues_IgnoresNull() {
assertTrue(BoolUtil.or(false, null, true));
assertFalse(BoolUtil.or(false, null, false));
}
@Test
void or_AllNullValues_ReturnsFalse() {
// Stream after filtering null is empty, anyMatch on empty returns false
assertFalse(BoolUtil.or((Boolean) null, null));
}
// Tests for or(BooleanSupplier... valueSuppliers)
@Test
void or_AllSuppliersTrue_ReturnsTrue() {
BooleanSupplier trueSupplier = () -> true;
BooleanSupplier falseSupplier = () -> false;
assertTrue(BoolUtil.or(trueSupplier, trueSupplier));
assertTrue(BoolUtil.or(falseSupplier, trueSupplier));
assertFalse(BoolUtil.or(falseSupplier, falseSupplier));
}
@Test
void or_WithNullSuppliers_IgnoresNull() {
BooleanSupplier trueSupplier = () -> true;
BooleanSupplier falseSupplier = () -> false;
assertTrue(BoolUtil.or(falseSupplier, null, trueSupplier));
assertFalse(BoolUtil.or(falseSupplier, null, falseSupplier));
}
@Test
void or_AllNullSuppliers_ReturnsFalse() {
assertFalse(BoolUtil.or((BooleanSupplier) null, null));
}
}
@@ -1,161 +0,0 @@
/*
* Copyright (C) 2024-2025 OnixByte.
*
* 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.
*/
package com.onixbyte.common.util;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import static org.junit.jupiter.api.Assertions.*;
class BranchUtilTest {
// Test the static methods or(Boolean... values) and and(Boolean... values)
@Test
void testOrWithBooleanValues() {
BranchUtil trueResult = BranchUtil.or(true, false, false);
assertNotNull(trueResult);
BranchUtil falseResult = BranchUtil.or(false, false, false);
assertNotNull(falseResult);
}
@Test
void testAndWithBooleanValues() {
BranchUtil trueResult = BranchUtil.and(true, true, true);
assertNotNull(trueResult);
BranchUtil falseResult = BranchUtil.and(true, false, true);
assertNotNull(falseResult);
}
// Test the static methods or(BooleanSupplier... valueSuppliers) and and(BooleanSupplier... valueSuppliers)
@Test
void testOrWithBooleanSuppliers() {
BooleanSupplier trueSupplier = () -> true;
BooleanSupplier falseSupplier = () -> false;
BranchUtil trueResult = BranchUtil.or(falseSupplier, trueSupplier);
BranchUtil falseResult = BranchUtil.or(falseSupplier, falseSupplier);
}
@Test
void testAndWithBooleanSuppliers() {
BooleanSupplier trueSupplier = () -> true;
BooleanSupplier falseSupplier = () -> false;
BranchUtil trueResult = BranchUtil.and(trueSupplier, trueSupplier);
BranchUtil falseResult = BranchUtil.and(trueSupplier, falseSupplier);
}
// Test thenSupply(T, T)
@Test
void testThenSupplyBothSuppliers_ResultTrue() {
BranchUtil b = BranchUtil.and(true);
String trueVal = "yes";
String falseVal = "no";
String result = b.thenSupply(() -> trueVal, () -> falseVal);
assertEquals(trueVal, result);
}
@Test
void testThenSupplyBothSuppliers_ResultFalse_WithFalseSupplier() {
BranchUtil b = BranchUtil.and(false);
String trueVal = "yes";
String falseVal = "no";
String result = b.thenSupply(() -> trueVal, () -> falseVal);
assertEquals(falseVal, result);
}
@Test
void testThenSupplyBothSuppliers_ResultFalse_NoFalseSupplier() {
BranchUtil b = BranchUtil.and(false);
String trueVal = "yes";
String result = b.thenSupply(() -> trueVal, null);
assertNull(result);
}
@Test
void testThenSupplySingleTrueSupplier_ResultTrue() {
BranchUtil b = BranchUtil.and(true);
String trueVal = "success";
String result = b.thenSupply(() -> trueVal);
assertEquals(trueVal, result);
}
@Test
void testThenSupplySingleTrueSupplier_ResultFalse() {
BranchUtil b = BranchUtil.and(false);
String trueVal = "success";
String result = b.thenSupply(() -> trueVal);
assertNull(result);
}
// Test then(Runnable, Runnable)
@Test
void testThenWithBothHandlers_ResultTrue() {
BranchUtil b = BranchUtil.and(true);
AtomicBoolean trueRun = new AtomicBoolean(false);
AtomicBoolean falseRun = new AtomicBoolean(false);
b.then(() -> trueRun.set(true), () -> falseRun.set(true));
assertTrue(trueRun.get());
assertFalse(falseRun.get());
}
@Test
void testThenWithBothHandlers_ResultFalse() {
BranchUtil b = BranchUtil.and(false);
AtomicBoolean trueRun = new AtomicBoolean(false);
AtomicBoolean falseRun = new AtomicBoolean(false);
b.then(() -> trueRun.set(true), () -> falseRun.set(true));
assertFalse(trueRun.get());
assertTrue(falseRun.get());
}
@Test
void testThenWithOnlyTrueHandler_ResultTrue() {
BranchUtil b = BranchUtil.and(true);
AtomicBoolean trueRun = new AtomicBoolean(false);
b.then(() -> trueRun.set(true));
assertTrue(trueRun.get());
}
@Test
void testThenWithOnlyTrueHandler_ResultFalse() {
BranchUtil b = BranchUtil.and(false);
AtomicBoolean trueRun = new AtomicBoolean(false);
b.then(() -> trueRun.set(true));
assertFalse(trueRun.get());
}
}
@@ -1,112 +0,0 @@
/*
* Copyright (C) 2024-2025 OnixByte.
*
* 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.
*/
package com.onixbyte.common.util;
import org.junit.jupiter.api.Test;
import java.util.*;
import java.util.function.Supplier;
import static org.junit.jupiter.api.Assertions.*;
class CollectionUtilTest {
@Test
void chunk_NullOriginalCollection_ThrowsException() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> CollectionUtil.chunk(null, 3, ArrayList::new));
assertEquals("Collection must not be null.", ex.getMessage());
}
@Test
void chunk_NegativeMaxSize_ThrowsException() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> CollectionUtil.chunk(List.of(1, 2), -1, ArrayList::new));
assertEquals("maxSize must greater than 0.", ex.getMessage());
}
@Test
void chunk_NullCollectionFactory_ThrowsException() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> CollectionUtil.chunk(List.of(1, 2), 2, null));
assertEquals("Factory method cannot be null.", ex.getMessage());
}
@Test
void chunk_EmptyCollection_ReturnsOneEmptySubCollection() {
List<List<Integer>> chunks = CollectionUtil.chunk(Collections.emptyList(), 3, ArrayList::new);
assertEquals(1, chunks.size());
assertTrue(chunks.get(0).isEmpty());
}
@Test
void chunk_CollectionSizeLessThanMaxSize_ReturnsOneSubCollectionWithAllElements() {
List<Integer> list = List.of(1, 2);
List<List<Integer>> chunks = CollectionUtil.chunk(list, 5, ArrayList::new);
assertEquals(1, chunks.size());
assertEquals(list, chunks.get(0));
}
@Test
void chunk_CollectionSizeEqualMaxSize_ReturnsOneSubCollectionWithAllElements() {
List<Integer> list = List.of(1, 2, 3);
List<List<Integer>> chunks = CollectionUtil.chunk(list, 3, ArrayList::new);
assertEquals(1, chunks.size());
assertEquals(list, chunks.get(0));
}
@Test
void chunk_CollectionSizeGreaterThanMaxSize_ReturnsMultipleSubCollections() {
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7);
int maxSize = 3;
List<List<Integer>> chunks = CollectionUtil.chunk(list, maxSize, ArrayList::new);
// Expect 3 subcollections: [1,2,3], [4,5,6], [7]
assertEquals(3, chunks.size());
assertEquals(List.of(1, 2, 3), chunks.get(0));
assertEquals(List.of(4, 5, 6), chunks.get(1));
assertEquals(List.of(7), chunks.get(2));
}
@Test
void chunk_UsesDifferentCollectionTypeAsSubCollections() {
LinkedList<Integer> list = new LinkedList<>(List.of(1, 2, 3, 4));
Supplier<LinkedList<Integer>> factory = LinkedList::new;
List<LinkedList<Integer>> chunks = CollectionUtil.chunk(list, 2, factory);
assertEquals(2, chunks.size());
assertInstanceOf(LinkedList.class, chunks.get(0));
assertInstanceOf(LinkedList.class, chunks.get(1));
assertEquals(List.of(1, 2), chunks.get(0));
assertEquals(List.of(3, 4), chunks.get(1));
}
@Test
void chunk_CollectionWithOneElementAndMaxSizeOne_ReturnsOneSubCollection() {
List<String> list = List.of("a");
List<List<String>> chunks = CollectionUtil.chunk(list, 1, ArrayList::new);
assertEquals(1, chunks.size());
assertEquals(list, chunks.get(0));
}
@Test
void chunk_MaxSizeZero_ThrowsException() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> CollectionUtil.chunk(List.of(1), 0, ArrayList::new));
assertEquals("maxSize must greater than 0.", ex.getMessage());
}
}
@@ -1,131 +0,0 @@
/*
* Copyright (C) 2024-2025 OnixByte.
*
* 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.
*/
package com.onixbyte.common.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class RangeUtilTest {
/**
* Tests generating ascending range from 0 up to end (exclusive).
*/
@Test
void testRangeEndValid() {
int[] expected = {0, 1, 2, 3, 4};
assertArrayEquals(expected, RangeUtil.range(5).toArray());
}
/**
* Tests that range(end) throws IllegalArgumentException for end less than or equal to zero.
*/
@Test
void testRangeEndInvalidThrows() {
IllegalArgumentException ex1 = assertThrows(IllegalArgumentException.class,
() -> RangeUtil.range(0));
assertTrue(ex1.getMessage().contains("should not be less than or equal to 0"));
IllegalArgumentException ex2 = assertThrows(IllegalArgumentException.class,
() -> RangeUtil.range(-3));
assertTrue(ex2.getMessage().contains("should not be less than or equal to 0"));
}
/**
* Tests ascending range where start is less than end.
*/
@Test
void testRangeStartEndAscending() {
int[] expected = {3, 4, 5, 6, 7};
assertArrayEquals(expected, RangeUtil.range(3, 8).toArray());
}
/**
* Tests descending range where start is greater than end.
*/
@Test
void testRangeStartEndDescending() {
int[] expected = {8, 7, 6, 5, 4};
assertArrayEquals(expected, RangeUtil.range(8, 3).toArray());
}
/**
* Tests empty stream when start equals end.
*/
@Test
void testRangeStartEqualsEndReturnsEmpty() {
assertEquals(0, RangeUtil.range(5, 5).count());
}
/**
* Tests that rangeClosed generates inclusive range in ascending order.
*/
@Test
void testRangeClosedAscending() {
int[] expected = {3, 4, 5, 6, 7, 8};
assertArrayEquals(expected, RangeUtil.rangeClosed(3, 8).toArray());
}
/**
* Tests range method with positive step generating ascending sequence.
*/
@Test
void testRangeWithPositiveStep() {
int[] expected = {2, 4, 6, 8};
assertArrayEquals(expected, RangeUtil.range(2, 10, 2).toArray());
}
/**
* Tests range method with negative step generating descending sequence.
*/
@Test
void testRangeWithNegativeStep() {
int[] expected = {10, 7, 4, 1};
assertArrayEquals(expected, RangeUtil.range(10, 0, -3).toArray());
}
/**
* Tests that passing zero step throws IllegalArgumentException.
*/
@Test
void testRangeStepZeroThrows() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> RangeUtil.range(0, 10, 0));
assertEquals("Step value must not be zero.", ex.getMessage());
}
/**
* Tests that range with positive step but invalid start/end throws IllegalArgumentException.
*/
@Test
void testRangePositiveStepInvalidRangeThrows() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> RangeUtil.range(10, 5, 1));
assertEquals("Range parameters are inconsistent with the step value.", ex.getMessage());
}
/**
* Tests that range with negative step but invalid start/end throws IllegalArgumentException.
*/
@Test
void testRangeNegativeStepInvalidRangeThrows() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> RangeUtil.range(5, 10, -1));
assertEquals("Range parameters are inconsistent with the step value.", ex.getMessage());
}
}
-113
View File
@@ -1,113 +0,0 @@
# Crypto Toolbox
Crypto Toolbox provides methods to simplify your codes on key pairs.
## ECDSA-based algorithm
### Generate key pair
#### Generate private key
Generate a private key by `genpkey` command provided by OpenSSL:
```shell
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec_private_key.pem
```
The output of this command is a file called `ec_private_key.pem` and its content looks like the
following:
```text
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs79JlARgXEf6EDV7
+PHQCTHEMtqIoHOy1GZ1+ynQJ6yhRANCAARkA7GRY2i4gg8qx0XViAXUP9cPw9pn
Jg1wfrQ41FaMyqVBejNYxvaLtamErF/ySimnjafMJ+VZCh34lBj6Ez8R
-----END PRIVATE KEY-----
```
#### Generate public key by private key
Export public key from private key with `ec` command provided by OpenSSL:
```shell
openssl ec -in ec_private_key.pem -pubout -out ec_public_key.pem
```
The output of this command is a file called `ec_public_key.pem` and its content looks like the
following:
```text
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZAOxkWNouIIPKsdF1YgF1D/XD8Pa
ZyYNcH60ONRWjMqlQXozWMb2i7WphKxf8kopp42nzCflWQod+JQY+hM/EQ==
-----END PUBLIC KEY-----
```
## RSA-based algorithm
### Generate key pair
#### Generate private key
Generate a private key by `genpkey` command provided by OpenSSL:
```shell
openssl genpkey -algorithm RSA -out rsa_private_key.pem -pkeyopt rsa_keygen_bits:2048
```
The output of this command is a file called `rsa_private_key.pem` and its content looks like the
following:
```text
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD4VIFYJFMAs15j
J3V3IicHd7sI2TIFqTZME40zlOlVAlPKLZmTQvZFLNgaUAAsvPi5i1DR2ywwK6Al
BfnwVnzvmDXC5mKHOz4oxOQVA6Nlp2yVaQMzidmfYNSkMtcv/4HRPsatc7K/M5l6
pCP20DVRjkikBdIy8e9w+x6BrIFp5Q8PZc/X2BGNAUMMYACdeYH5R/A0CxqkND13
esc4gkynMOrvZrZGHCz51usfSCqyDWWwsN+GG6LYWia4GkNlS0erQnP8gS93dfjl
e96BIfy3z7Iv+kUrf5ikNW2P8jMxLAv6LO+dcUAu9k477wIAF7Iq5KMuH/otsDOu
+h+2qXmBAgMBAAECggEAdRqcmC0g+y6arxV3fkObthjPGYAa57KBCWUa7B0n30+m
pavVRS2Jpttb2SSqwG4ouI6rARti/iBEd9EWqTCP4AieKZetFOpqCJ24lPRPRGus
d9S6jr5N4qut+vSCp37NABijZj4uJ540nTH0R7qtuhTnynl4Q0/1wwiYvTvVF1Lg
dn+I/8aRbshwDhdAOWOUe6GL7/eaCYgN8/UmlKIpp8tg0w2iWxbaFiR7gZiM41LA
M6SXXfcCas+ZVXsGbzQ3SNiVurCGuuRNcCScXS3/WoEDIb3cNtp49iOmQS+nmEoo
wh4uiEd+0+BrzxngS4o5+mKnHJnwgY0+veGVYLMR5QKBgQD9WKQmevMDU5c+NPq9
8jaR457Fuxq1gwzeFNJdWfOc/K2LEWh+nFNFCb++EboEj6FdxWaWNMxbrmJps5gs
EoBUYy/Tl7UycDqDfiYLmDdTsf2pVjjh9jaIADiLcJ8S6wwJMZKub7Tp8UVkenAl
535MqShLUC11Y7VxLb3Tsll4XwKBgQD67mm6iCmshr/eszPfNE3ylZ+PiNa7nat7
N7lQzBIiRJflT1kmVidC5gE+jASqH728ChkZZKxbHsjxpmWdAhLOITdXoTB4sDsd
wtV1lxkXxK9FnrpFvO3y1wZ/QsD3Z2KXxHYZqawkUETO9F3nqAXW0b2GDar5Qiyo
J3Tx/43aHwKBgDC0NMJtCoDONhowZy/S+6iqQKC0qprQec3L5PErVMkOTnKYwyTr
+pogGKt6ju9HiXcUdvdTaSIK8UJu00dNuzv94XjlBmGO78DNpJTAC4rcge5m9AKE
qdEVcclkukARzbuKuy8rrHT4/CUn4J141m/4aRWpcUPLCluato6XD9ozAoGBANvf
JhOFFgcPd3YazfvpZ9eE1XA+tfFlYYmxNRcgCU+vjO0oDvSxjutmgHae18N91pG6
w21lskSRf/+GDwl5dKLbphOJsOA/gz07qDDGOf2CoRW+1Hcg6drcINxH0K+4DkLv
qZApBSY4k2JH6zR+HMeztn6M4WBRZLHfCPC3PUN/AoGAA3AoHbLTZvqMIKSDkP4Y
U/tTsSFDY4aYo7LG/jk8af3oPU3KyGh4ZFBd6aMmXbS8f8FjvmrM+/e+y9OOGAlq
iOl0hYrs5cJSMLW6i4KnJYuYbMkgmk3bN2t9apu64xKR94gbPrI6AGnPZp+iIzp0
hXKe4HcuhQ3G0a2hjayiQ84=
-----END PRIVATE KEY-----
```
#### Generate public key by private key
Export public key from private key by OpenSSL:
```shell
openssl pkey -in rsa_private_key.pem -pubout -out rsa_public_key.pem
```
The output of this command is a file called `rsa_public_key.pem` and its content looks like the
following:
```text
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+FSBWCRTALNeYyd1dyIn
B3e7CNkyBak2TBONM5TpVQJTyi2Zk0L2RSzYGlAALLz4uYtQ0dssMCugJQX58FZ8
75g1wuZihzs+KMTkFQOjZadslWkDM4nZn2DUpDLXL/+B0T7GrXOyvzOZeqQj9tA1
UY5IpAXSMvHvcPsegayBaeUPD2XP19gRjQFDDGAAnXmB+UfwNAsapDQ9d3rHOIJM
pzDq72a2Rhws+dbrH0gqsg1lsLDfhhui2FomuBpDZUtHq0Jz/IEvd3X45XvegSH8
t8+yL/pFK3+YpDVtj/IzMSwL+izvnXFALvZOO+8CABeyKuSjLh/6LbAzrvoftql5
gQIDAQAB
-----END PUBLIC KEY-----
```
-135
View File
@@ -1,135 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import java.net.URI
plugins {
java
id("java-library")
id("maven-publish")
id("signing")
}
val cryptoToolboxVersion: String by project
version = cryptoToolboxVersion
val projectUrl: String by project
val projectGithubUrl: String by project
val licenseName: String by project
val licenseUrl: String by project
repositories {
mavenCentral()
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
withSourcesJar()
withJavadocJar()
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
tasks.withType<Jar> {
exclude("logback.xml")
}
dependencies {
compileOnly(libs.slf4j)
implementation(libs.logback)
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}
publishing {
publications {
create<MavenPublication>("cryptoToolbox") {
groupId = group.toString()
artifactId = "crypto-toolbox"
version = cryptoToolboxVersion
pom {
name = "OnixByte Crypto Toolbox"
description =
"This module can easily load key pairs from a PEM content."
url = projectUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
scm {
connection = "scm:git:git://github.com:onixbyte/onixbyte-toolbox.git"
developerConnection = "scm:git:git://github.com:onixbyte/onixbyte-toolbox.git"
url = projectGithubUrl
}
developers {
developer {
id = "zihluwang"
name = "Zihlu Wang"
email = "really@zihlu.wang"
timezone = "Asia/Hong_Kong"
}
developer {
id = "siujamo"
name = "Siu Jam'o"
email = "jamo.siu@outlook.com"
timezone = "Asia/Shanghai"
}
}
}
from(components["java"])
signing {
setRequired(project.hasProperty("signing.keyId"))
sign(publishing.publications["cryptoToolbox"])
}
}
repositories {
maven {
name = "sonatypeNexus"
url = URI(providers.gradleProperty("repo.maven-central.host").get())
credentials {
username = providers.gradleProperty("repo.maven-central.username").get()
password = providers.gradleProperty("repo.maven-central.password").get()
}
}
}
}
}
@@ -1,44 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto;
import java.security.PrivateKey;
/**
* The {@code PrivateKeyLoader} provides utility methods for loading private keys from
* PEM-formatted key text.
*
* @author zihluwang
* @author siujamo
* @version 3.0.0
*/
public interface PrivateKeyLoader {
/**
* Load private key from pem-formatted key text.
*
* @param pemKeyText pem-formatted key text
* @return loaded private key
*/
PrivateKey loadPrivateKey(String pemKeyText);
}
@@ -1,83 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto;
import com.onixbyte.crypto.exception.KeyLoadingException;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
/**
* The {@code PublicKeyLoader} provides utility methods for loading public keys from PEM-formatted
* key text.
*
* @author zihluwang
* @author siujamo
* @version 3.0.0
*/
public interface PublicKeyLoader {
/**
* Load public key from PEM-formatted key text.
*
* @param pemKeyText PEM-formatted key text
* @return loaded private key
*/
PublicKey loadPublicKey(String pemKeyText);
/**
* Loads an EC public key using the provided x and y coordinates together with the curve name.
* <p>
* This default implementation throws a {@link KeyLoadingException} to signify that this key
* loader does not support loading an EC public key. Implementing classes are expected to
* override this method to supply their own loading logic.
*
* @param xHex the hexadecimal string representing the x coordinate of the EC point
* @param yHex the hexadecimal string representing the y coordinate of the EC point
* @param curveName the name of the elliptic curve
* @return the loaded {@link ECPublicKey} instance
* @throws KeyLoadingException if loading is not supported or fails
*/
default ECPublicKey loadPublicKey(String xHex, String yHex, String curveName) {
throw new KeyLoadingException("This key loader does not support loading an EC public key.");
}
/**
* Loads an RSA public key using the provided modulus and exponent.
* <p>
* This default implementation throws a {@link KeyLoadingException} to signify that this key
* loader does not support loading an RSA public key. Implementing classes are expected to
* override this method to supply their own loading logic.
*
* @param modulus the modulus value of the RSA public key, usually represented in hexadecimal
* or Base64 string format
* @param exponent the public exponent value of the RSA public key, usually represented in
* hexadecimal or Base64 string format
* @return the loaded {@link RSAPublicKey} instance
* @throws KeyLoadingException if loading is not supported or fails
*/
default RSAPublicKey loadPublicKey(String modulus, String exponent) {
throw new KeyLoadingException("This key loader does not support loading an RSA public key.");
}
}
@@ -1,95 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto.algorithm.ecdsa;
import com.onixbyte.crypto.PrivateKeyLoader;
import com.onixbyte.crypto.exception.KeyLoadingException;
import com.onixbyte.crypto.util.CryptoUtil;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.*;
import java.util.Base64;
/**
* A class responsible for loading private ECDSA keys from PEM formatted text.
* <p>
* This class implements the {@link PrivateKeyLoader} interface and provides methods to load private
* RSA keys. The keys are expected to be in the standard PEM format, which includes Base64-encoded
* key content surrounded by header and footer lines. The class handles the decoding of Base64
* content and the generation of keys using the RSA key factory.
* <p>
* Any exceptions encountered during the loading process are encapsulated in a
* {@link KeyLoadingException}, allowing for flexible error handling.
*
* @author zihluwang
* @author siujamo
* @version 3.0.0
* @see PrivateKeyLoader
* @see KeyLoadingException
*/
public class ECPrivateKeyLoader implements PrivateKeyLoader {
private final KeyFactory keyFactory;
private final Base64.Decoder decoder;
/**
* Initialise a key loader for EC-based algorithms.
*/
public ECPrivateKeyLoader() {
try {
this.keyFactory = KeyFactory.getInstance("EC");
this.decoder = Base64.getDecoder();
} catch (NoSuchAlgorithmException e) {
throw new KeyLoadingException(e);
}
}
/**
* Load ECDSA private key from pem-formatted key text.
*
* @param pemKeyText pem-formatted key text
* @return loaded private key
* @throws KeyLoadingException if the generated key is not a {@link ECPrivateKey} instance,
* or EC Key Factory is not loaded, or key spec is invalid
*/
@Override
public ECPrivateKey loadPrivateKey(String pemKeyText) {
try {
pemKeyText = CryptoUtil.getRawContent(pemKeyText);
var decodedKeyString = decoder.decode(pemKeyText);
var keySpec = new PKCS8EncodedKeySpec(decodedKeyString);
var _key = keyFactory.generatePrivate(keySpec);
if (_key instanceof ECPrivateKey privateKey) {
return privateKey;
} else {
throw new KeyLoadingException("Unable to load private key from pem-formatted key text.");
}
} catch (InvalidKeySpecException e) {
throw new KeyLoadingException("Key spec is invalid.", e);
}
}
}
@@ -1,159 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto.algorithm.ecdsa;
import com.onixbyte.crypto.PrivateKeyLoader;
import com.onixbyte.crypto.PublicKeyLoader;
import com.onixbyte.crypto.exception.KeyLoadingException;
import com.onixbyte.crypto.util.CryptoUtil;
import java.math.BigInteger;
import java.security.AlgorithmParameters;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.*;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;
/**
* A class responsible for loading public ECDSA keys from PEM formatted text.
* <p>
* This class implements the {@link PublicKeyLoader} interface and provides methods to load private
* RSA keys. The keys are expected to be in the standard PEM format, which includes Base64-encoded
* key content surrounded by header and footer lines. The class handles the decoding of Base64
* content and the generation of keys using the RSA key factory.
* <p>
* Any exceptions encountered during the loading process are encapsulated in a
* {@link KeyLoadingException}, allowing for flexible error handling.
*
* @author zihluwang
* @author siujamo
* @version 3.0.0
* @see PrivateKeyLoader
* @see KeyLoadingException
*/
public class ECPublicKeyLoader implements PublicKeyLoader {
/**
* Supported curves.
*/
public static final Set<String> SUPPORTED_CURVES = new HashSet<>(Set.of(
"secp256r1", "secp384r1", "secp521r1", "secp224r1"
));
private final KeyFactory keyFactory;
private final Base64.Decoder decoder;
/**
* Initialise a key loader for EC-based algorithms.
*/
public ECPublicKeyLoader() {
try {
this.keyFactory = KeyFactory.getInstance("EC");
this.decoder = Base64.getDecoder();
} catch (NoSuchAlgorithmException e) {
throw new KeyLoadingException(e);
}
}
/**
* Load public key from pem-formatted key text.
*
* @param pemKeyText pem-formatted key text
* @return loaded private key
* @throws KeyLoadingException if the generated key is not a {@link ECPrivateKey} instance,
* or EC Key Factory is not loaded, or key spec is invalid
*/
@Override
public ECPublicKey loadPublicKey(String pemKeyText) {
try {
pemKeyText = CryptoUtil.getRawContent(pemKeyText);
var keyBytes = decoder.decode(pemKeyText);
var spec = new X509EncodedKeySpec(keyBytes);
var key = keyFactory.generatePublic(spec);
if (key instanceof ECPublicKey publicKey) {
return publicKey;
} else {
throw new KeyLoadingException("Unable to load public key from pem-formatted key text.");
}
} catch (InvalidKeySpecException e) {
throw new KeyLoadingException("Key spec is invalid.", e);
}
}
/**
* Loads an EC public key from the given hexadecimal x and y coordinates alongside the curve name.
* <p>
* This method converts the hexadecimal string representations of the EC point coordinates into {@link BigInteger}
* instances, then constructs an {@link ECPoint} and retrieves the corresponding {@link ECParameterSpec} for the
* named curve. Subsequently, it utilises the {@link KeyFactory} to generate an {@link ECPublicKey}.
* <p>
* Only curves listed in {@link #SUPPORTED_CURVES} are supported. Should the specified curve name be unsupported,
* or if key construction fails due to invalid parameters or unsupported algorithms, a {@link KeyLoadingException}
* will be thrown.
*
* @param xHex the hexadecimal string representing the x-coordinate of the EC point
* @param yHex the hexadecimal string representing the y-coordinate of the EC point
* @param curveName the name of the elliptic curve
* @return the {@link ECPublicKey} generated from the specified coordinates and curve
* @throws KeyLoadingException if the curve is unsupported or key generation fails
*/
@Override
public ECPublicKey loadPublicKey(String xHex, String yHex, String curveName) {
if (!SUPPORTED_CURVES.contains(curveName)) {
throw new KeyLoadingException("Given curve is not supported yet.");
}
try {
// Convert hex string coordinates to BigInteger
var x = new BigInteger(xHex, 16);
var y = new BigInteger(yHex, 16);
// Create ECPoint with (x, y)
var ecPoint = new ECPoint(x, y);
// Get EC parameter spec for the named curve
var parameters = AlgorithmParameters.getInstance("EC");
parameters.init(new ECGenParameterSpec(curveName));
var ecParameterSpec = parameters.getParameterSpec(ECParameterSpec.class);
// Create ECPublicKeySpec with point and curve params
var pubSpec = new ECPublicKeySpec(ecPoint, ecParameterSpec);
// Generate public key using KeyFactory
var publicKey = keyFactory.generatePublic(pubSpec);
if (publicKey instanceof ECPublicKey ecPublicKey) {
return ecPublicKey;
} else {
throw new KeyLoadingException("Cannot load EC public key with given x, y and curve name.");
}
} catch (NoSuchAlgorithmException | InvalidParameterSpecException | InvalidKeySpecException e) {
throw new KeyLoadingException("Cannot load EC public key with given x, y and curve name.", e);
}
}
}
@@ -1,107 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto.algorithm.rsa;
import com.onixbyte.crypto.PrivateKeyLoader;
import com.onixbyte.crypto.exception.KeyLoadingException;
import com.onixbyte.crypto.util.CryptoUtil;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.*;
import java.util.Base64;
/**
* A class responsible for loading private RSA keys from PEM formatted text.
* <p>
* This class implements the {@link PrivateKeyLoader} interface and provides methods to load private
* RSA keys. The keys are expected to be in the standard PEM format, which includes Base64-encoded
* key content surrounded by header and footer lines. The class handles the decoding of Base64
* content and the generation of keys using the RSA key factory.
* <p>
* Any exceptions encountered during the loading process are encapsulated in a
* {@link KeyLoadingException}, allowing for flexible error handling.
*
* @author zihluwang
* @author siujamo
* @version 3.0.0
* @see PrivateKeyLoader
* @see KeyLoadingException
*/
public class RSAPrivateKeyLoader implements PrivateKeyLoader {
private final Base64.Decoder decoder;
private final KeyFactory keyFactory;
/**
* Constructs an instance of {@code RsaKeyLoader}.
* <p>
* This constructor initialises the Base64 decoder and the RSA {@link KeyFactory}. It may throw
* a {@link KeyLoadingException} if the RSA algorithm is not available.
*/
public RSAPrivateKeyLoader() {
try {
this.decoder = Base64.getDecoder();
this.keyFactory = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new KeyLoadingException(e);
}
}
/**
* Loads an RSA private key from a given PEM formatted key text.
* <p>
* This method extracts the raw key content from the provided PEM text, decodes the
* Base64-encoded content, and generates an instance of {@link RSAPrivateKey}. If the key cannot
* be loaded due to invalid specifications or types, a {@link KeyLoadingException} is thrown.
*
* @param pemKeyText the PEM formatted private key text
* @return an instance of {@link RSAPrivateKey}
* @throws KeyLoadingException if the key loading process encounters an error
*/
@Override
public RSAPrivateKey loadPrivateKey(String pemKeyText) {
// Extract the raw key content
var rawKeyContent = CryptoUtil.getRawContent(pemKeyText);
// Decode the Base64-encoded content
var keyBytes = decoder.decode(rawKeyContent);
// Create a PKCS8EncodedKeySpec from the decoded bytes
var keySpec = new PKCS8EncodedKeySpec(keyBytes);
try {
// Get an RSA KeyFactory and generate the private key
var _key = keyFactory.generatePrivate(keySpec);
if (_key instanceof RSAPrivateKey key) {
return key;
} else {
throw new KeyLoadingException("Unable to load private key from pem-formatted key text.");
}
} catch (InvalidKeySpecException e) {
throw new KeyLoadingException("Key spec is invalid.", e);
}
}
}
@@ -1,142 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto.algorithm.rsa;
import com.onixbyte.crypto.PrivateKeyLoader;
import com.onixbyte.crypto.PublicKeyLoader;
import com.onixbyte.crypto.exception.KeyLoadingException;
import com.onixbyte.crypto.util.CryptoUtil;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* A class responsible for loading public RSA keys from PEM formatted text.
* <p>
* This class implements the {@link PublicKeyLoader} interface and provides methods to load public
* RSA keys. The keys are expected to be in the standard PEM format, which includes Base64-encoded
* key content surrounded by header and footer lines. The class handles the decoding of Base64
* content and the generation of keys using the RSA key factory.
* <p>
* Any exceptions encountered during the loading process are encapsulated in a
* {@link KeyLoadingException}, allowing for flexible error handling.
*
* @author zihluwang
* @author siujamo
* @version 3.0.0
* @see PrivateKeyLoader
* @see KeyLoadingException
*/
public class RSAPublicKeyLoader implements PublicKeyLoader {
private final Base64.Decoder decoder;
private final Base64.Decoder urlDecoder;
private final KeyFactory keyFactory;
/**
* Constructs an instance of {@code RsaKeyLoader}.
* <p>
* This constructor initialises the Base64 decoder and the RSA {@link KeyFactory}. It may throw
* a {@link KeyLoadingException} if the RSA algorithm is not available.
*/
public RSAPublicKeyLoader() {
try {
this.decoder = Base64.getDecoder();
this.urlDecoder = Base64.getUrlDecoder();
this.keyFactory = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new KeyLoadingException(e);
}
}
/**
* Loads an RSA public key from a given PEM formatted key text.
* <p>
* This method extracts the raw key content from the provided PEM text, decodes the
* Base64-encoded content, and generates an instance of {@link RSAPublicKey}. If the key cannot
* be loaded due to invalid specifications or types, a {@link KeyLoadingException} is thrown.
*
* @param pemKeyText the PEM formatted public key text
* @return an instance of {@link RSAPublicKey}
* @throws KeyLoadingException if the key loading process encounters an error
*/
@Override
public RSAPublicKey loadPublicKey(String pemKeyText) {
// Extract the raw key content
var rawKeyContent = CryptoUtil.getRawContent(pemKeyText);
// Decode the Base64-encoded content
var keyBytes = decoder.decode(rawKeyContent);
// Create an X509EncodedKeySpec from the decoded bytes
var keySpec = new X509EncodedKeySpec(keyBytes);
// Get an RSA KeyFactory and generate the public key
try {
var _key = keyFactory.generatePublic(keySpec);
if (_key instanceof RSAPublicKey key) {
return key;
} else {
throw new KeyLoadingException("Unable to load public key from pem-formatted key text.");
}
} catch (InvalidKeySpecException e) {
throw new KeyLoadingException("Key spec is invalid.", e);
}
}
/**
* Get the public key with given modulus and public exponent.
*
* @param modulus the modulus
* @param exponent the public exponent
* @return generated public key object from the provided key specification
* @see KeyFactory#getInstance(String)
* @see KeyFactory#generatePublic(KeySpec)
*/
@Override
public RSAPublicKey loadPublicKey(String modulus, String exponent) {
try {
var _modulus = new BigInteger(1, urlDecoder.decode(modulus));
var _exponent = new BigInteger(1, urlDecoder.decode(exponent));
var keySpec = new RSAPublicKeySpec(_modulus, _exponent);
var kf = KeyFactory.getInstance("RSA");
if (kf.generatePublic(keySpec) instanceof RSAPublicKey rsaPublicKey) {
return rsaPublicKey;
} else {
throw new KeyLoadingException("Cannot generate RSA public key with given modulus and exponent.");
}
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new KeyLoadingException("Cannot generate RSA public key with given modulus and exponent.", e);
}
}
}
@@ -1,100 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto.exception;
/**
* The {@code KeyLoadingException} class represents an exception that is thrown when there is an
* error loading cryptographic keys. This exception can be used to indicate various issues such as
* invalid key specifications, unsupported key algorithms, or other key loading errors.
* <p>
* This class extends {@link RuntimeException}, allowing it to be thrown without being declared in
* a method's {@code throws} clause.
* </p>
*
* <p><b>Example usage:</b></p>
* <pre>{@code
* try {
* PrivateKeyLoader keyLoader = new ECPrivateKeyLoader();
* ECPrivateKey privateKey = keyLoader.loadPrivateKey(pemPrivateKey);
* } catch (KeyLoadingException e) {
* // Handle the exception
* e.printStackTrace();
* }
* }</pre>
*
* @author zihluwang
* @version 3.0.0
*/
public class KeyLoadingException extends RuntimeException {
/**
* Creates a new instance of {@code KeyLoadingException} without a specific message or cause.
*/
public KeyLoadingException() {
}
/**
* Creates a new instance of {@code KeyLoadingException} with the specified detail message.
*
* @param message the detail message
*/
public KeyLoadingException(String message) {
super(message);
}
/**
* Creates a new instance of {@code KeyLoadingException} with the specified detail message
* and cause.
*
* @param message the detail message
* @param cause the cause of this exception
*/
public KeyLoadingException(String message, Throwable cause) {
super(message, cause);
}
/**
* Creates a new instance of {@code KeyLoadingException} with the specified cause.
*
* @param cause the cause of this exception
*/
public KeyLoadingException(Throwable cause) {
super(cause);
}
/**
* Constructs a new exception with the specified detail message, cause, suppression enabled
* or disabled, and writable stack trace enabled or disabled.
*
* @param message the detail message
* @param cause the cause of this exception
* @param enableSuppression whether suppression is enabled or disabled
* @param writableStackTrace whether the stack trace should be writable
*/
public KeyLoadingException(String message,
Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
@@ -1,98 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto.util;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* Utility class for cryptographic operations.
*
* @author zihluwang
* @author siujamo
* @version 3.0.0
*/
public final class CryptoUtil {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private CryptoUtil() {
}
/**
* Extracts the raw content from a PEM-formatted key by removing any headers, footers,
* and newline characters.
*
* <p>
* This method processes the given PEM key text and returns a cleaned string containing only
* the key material. It removes the lines matching the
* {@code "-----BEGIN (EC )?(RSA )?(PRIVATE|PUBLIC) KEY-----"} and
* {@code "-----END (EC )?(RSA )?(PRIVATE|PUBLIC) KEY-----"} patterns,
* as well as any newline characters,
* resulting in a continuous string that can be used directly for cryptographic operations.
*
* @param pemKeyText the PEM-formatted key as a string, which may include headers, footers,
* and line breaks
* @return a string containing the raw key content without any unnecessary formatting or whitespace
*/
public static String getRawContent(String pemKeyText) {
return pemKeyText
.replaceAll("-----BEGIN ((EC )|(RSA ))?(PRIVATE|PUBLIC) KEY-----", "")
.replaceAll("-----END ((EC )|(RSA ))?(PRIVATE|PUBLIC) KEY-----", "")
.replace("\n", "");
}
/**
* Computes a Hash-based Message Authentication Code (HMAC) using the SHA-256 algorithm.
* <p>
* The input payload and secret key are both processed using the {@code UTF-8} charset
* to guarantee consistent results across different operating system environments.
* The final byte array output is converted into a lower-case hexadecimal string.
*
* @param payload the raw string data or message content to be authenticated
* @param secret the secret key used to sign the payload
* @return a lower-case hexadecimal string representing the computed HMAC-SHA256 signature
* @throws NoSuchAlgorithmException if the HmacSHA256 algorithm is not available in the environment
* @throws InvalidKeyException if the provided secret key is inappropriate for
* initialising the MAC
*/
public static String hmacSha256(
String payload,
String secret
) throws NoSuchAlgorithmException, InvalidKeyException {
var secretKeySpec = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
var mac = Mac.getInstance("HmacSHA256");
mac.init(secretKeySpec);
var rawHmac = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return EncodingUtil.bytesToHex(rawHmac);
}
}
@@ -1,53 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto.util;
import java.util.HexFormat;
/**
* Utility class for handling various data encoding and formatting operations.
* <p>
* This class provides helper methods to convert raw binary data into standardised
* string representations (such as hexadecimal format) commonly required for
* cryptographic verification, logging, and data transmission.
* <p>
* This utility class is stateless and thread-safe.
*
* @author siujamo
*/
public class EncodingUtil {
/**
* Converts an array of bytes into its corresponding hexadecimal string representation.
* <p>
* Each byte is converted to a two-digit hex string. If the resulting hex value
* is a single digit (i.e., less than 16), a leading '0' is automatically prepended
* to ensure a consistent and uniform format throughout the output string.
*
* @param bytes the byte array to be converted, typically the raw output of a cryptographic hash
* @return a lower-case hexadecimal string representing the input bytes
*/
public static String bytesToHex(byte[] bytes) {
return HexFormat.of().formatHex(bytes);
}
}
@@ -1,270 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.crypto.util;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;
/**
* The {@code HashUtil} class provides convenient methods for calculating various hash functions on
* strings, including MD2, MD5, SHA-1, SHA-224, SHA-256, SHA-384, and SHA-512. It allows developers
* to easily obtain the hash value of a given string using different algorithms.
* <p>
* Example usage:
* <pre>
* // Perform MD2 hash operation
* String md2Hash = HashUtil.md2("someString");
*
* // Perform MD5 hash operation
* String md5Hash = HashUtil.md5("someString");
*
* // Perform SHA-1 hash operation
* String sha1Hash = HashUtil.sha1("someString");
*
* // Perform SHA-224 hash operation
* String sha224Hash = HashUtil.sha224("someString");
*
* // Perform SHA-256 hash operation
* String sha256Hash = HashUtil.sha256("someString");
*
* // Perform SHA-384 hash operation
* String sha384Hash = HashUtil.sha384("someString");
*
* // Perform SHA-512 hash operation
* String sha512Hash = HashUtil.sha512("someString");
* </pre>
* The above examples demonstrate how to use the {@code HashUtil} class to calculate hash values
* for a given string using different algorithms.
* <p>
* The hash functions provided by the {@link HashUtil} are one-way hash functions, meaning the
* original data cannot be retrieved from the hash value. These hash functions are commonly used
* for data integrity checks and password storage, but they should not be used for
* encryption purposes.
*
* @author zihluwang
* @version 3.0.0
* @see MessageDigest
*/
public final class HashUtil {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private HashUtil() {
}
/**
* Calculates the MD2 hash value of the specified string using the given charset.
*
* @param value the string to calculate with the MD2 algorithm
* @param charset the charset to use for encoding the string (default is UTF-8 if null)
* @return the MD2 hash value as a hexadecimal string
*/
public static String md2(String value, Charset charset) {
charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
return hash("MD2", value, charset);
}
/**
* Calculates the MD2 hash value of the specified string using the UTF-8 charset.
*
* @param value the string to calculate with the MD2 algorithm
* @return the MD2 hash value as a hexadecimal string
*/
public static String md2(String value) {
return hash("MD2", value, StandardCharsets.UTF_8);
}
/**
* Calculates the MD5 hash value of the specified string using the given charset.
*
* @param value the string to calculate with the MD5 algorithm
* @param charset the charset to use for encoding the string (default is UTF-8 if null)
* @return the MD5 hash value as a hexadecimal string
*/
public static String md5(String value, Charset charset) {
charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
return hash("MD5", value, charset);
}
/**
* Calculates the MD5 hash value of the specified string using the UTF-8 charset.
*
* @param value the string to calculate with the MD5 algorithm
* @return the MD5 hash value as a hexadecimal string
*/
public static String md5(String value) {
return hash("MD5", value, StandardCharsets.UTF_8);
}
/**
* Calculates the SHA-1 hash value of the specified string using the given charset.
*
* @param value the string to calculate with the SHA-1 algorithm
* @param charset the charset to use for encoding the string (default is UTF-8 if null)
* @return the SHA-1 hash value as a hexadecimal string
*/
public static String sha1(String value, Charset charset) {
charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
return hash("SHA-1", value, charset);
}
/**
* Calculates the SHA-1 hash value of the specified string using the UTF-8 charset.
*
* @param value the string to calculate with the SHA-1 algorithm
* @return the SHA-1 hash value as a hexadecimal string
*/
public static String sha1(String value) {
return hash("SHA-1", value, StandardCharsets.UTF_8);
}
/**
* Calculates the SHA-224 hash value of the specified string using the given charset.
*
* @param value the string to calculate with the SHA-225 algorithm
* @param charset the charset to use for encoding the string (default is UTF-8 if null)
* @return the SHA-224 hash value as a hexadecimal string
*/
public static String sha224(String value, Charset charset) {
charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
return hash("SHA-224", value, charset);
}
/**
* Calculates the SHA-224 hash value of the specified string using the
* UTF-8 charset.
*
* @param value the string to calculate with the SHA-224 algorithm
* @return the SHA-224 hash value as a hexadecimal string
*/
public static String sha224(String value) {
return hash("SHA-224", value, StandardCharsets.UTF_8);
}
/**
* Calculates the SHA-256 hash value of the specified string using the
* given charset.
*
* @param value the string to calculate with the SHA-256 algorithm
* @param charset the charset to use for encoding the string (default is UTF-8 if null)
* @return the SHA-256 hash value as a hexadecimal string
*/
public static String sha256(String value, Charset charset) {
charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
return hash("SHA-256", value, charset);
}
/**
* Calculates the SHA-256 hash value of the specified string using the UTF-8 charset.
*
* @param value the string to calculate with the SHA-256 algorithm
* @return the SHA-256 hash value as a hexadecimal string
*/
public static String sha256(String value) {
return hash("SHA-256", value, StandardCharsets.UTF_8);
}
/**
* Calculates the SHA-384 hash value of the specified string using the given charset.
*
* @param value the string to calculate with the SHA-384 algorithm
* @param charset the charset to use for encoding the string (default is UTF-8 if null)
* @return the SHA-384 hash value as a hexadecimal string
*/
public static String sha384(String value, Charset charset) {
charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
return hash("SHA-384", value, charset);
}
/**
* Calculates the SHA-384 hash value of the specified string using the UTF-8 charset.
*
* @param value the string to calculate with the SHA-384 algorithm
* @return the SHA-384 hash value as a hexadecimal string
*/
public static String sha384(String value) {
return hash("SHA-384", value, StandardCharsets.UTF_8);
}
/**
* Calculates the SHA-512 hash value of the specified string using the given charset.
*
* @param value the string to calculate with the SHA-512 algorithm
* @param charset the charset to use for encoding the string (default is UTF-8 if null)
* @return the SHA-512 hash value as a hexadecimal string
*/
public static String sha512(String value, Charset charset) {
charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
return hash("SHA-512", value, charset);
}
/**
* Calculates the SHA-512 hash value of the specified string using the UTF-8 charset.
*
* @param value the string to calculate with the SHA-512 algorithm
* @return the SHA-512 hash value as a hexadecimal string
*/
public static String sha512(String value) {
return hash("SHA-512", value, StandardCharsets.UTF_8);
}
/**
* Calculates the hash value of the specified string using the specified
* algorithm and charset.
*
* @param method the hash algorithm to use
* @param value the string to calculate the hash value for
* @param charset the charset to use for encoding the string (default is UTF-8 if null)
* @return the hash value as a hexadecimal string, or an empty string if the algorithm is
* not available
* @throws RuntimeException if an unknown algorithm name is provided (should not occur under
* controlled usage)
*/
private static String hash(String method, String value, Charset charset) {
try {
var messageDigest = MessageDigest.getInstance(method);
messageDigest.update(value.getBytes(charset));
var bytes = messageDigest.digest();
var builder = new StringBuilder();
for (var b : bytes) {
var str = Integer.toHexString(b & 0xff);
if (str.length() == 1) {
builder.append(0);
}
builder.append(str);
}
return builder.toString();
} catch (NoSuchAlgorithmException ignored) {
// This should not occur under controlled usage
// Only trusted algorithms are allowed
return "";
}
}
}
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2024-2026 OnixByte
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<configuration>
<property name="COLOURFUL_OUTPUT"
value="%black(%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK}) %highlight(%-5level) %black(---) %black([%10.10t]) %cyan(%-20.20logger{20}) %black(:) %msg%n"/>
<property name="STANDARD_OUTPUT"
value="%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK} %-5level %black(---) [%10.10t] %-20.20logger{20} : %msg%n"/>
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${COLOURFUL_OUTPUT}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
@@ -1,5 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs79JlARgXEf6EDV7
+PHQCTHEMtqIoHOy1GZ1+ynQJ6yhRANCAARkA7GRY2i4gg8qx0XViAXUP9cPw9pn
Jg1wfrQ41FaMyqVBejNYxvaLtamErF/ySimnjafMJ+VZCh34lBj6Ez8R
-----END PRIVATE KEY-----
@@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZAOxkWNouIIPKsdF1YgF1D/XD8Pa
ZyYNcH60ONRWjMqlQXozWMb2i7WphKxf8kopp42nzCflWQod+JQY+hM/EQ==
-----END PUBLIC KEY-----
@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD4VIFYJFMAs15j
J3V3IicHd7sI2TIFqTZME40zlOlVAlPKLZmTQvZFLNgaUAAsvPi5i1DR2ywwK6Al
BfnwVnzvmDXC5mKHOz4oxOQVA6Nlp2yVaQMzidmfYNSkMtcv/4HRPsatc7K/M5l6
pCP20DVRjkikBdIy8e9w+x6BrIFp5Q8PZc/X2BGNAUMMYACdeYH5R/A0CxqkND13
esc4gkynMOrvZrZGHCz51usfSCqyDWWwsN+GG6LYWia4GkNlS0erQnP8gS93dfjl
e96BIfy3z7Iv+kUrf5ikNW2P8jMxLAv6LO+dcUAu9k477wIAF7Iq5KMuH/otsDOu
+h+2qXmBAgMBAAECggEAdRqcmC0g+y6arxV3fkObthjPGYAa57KBCWUa7B0n30+m
pavVRS2Jpttb2SSqwG4ouI6rARti/iBEd9EWqTCP4AieKZetFOpqCJ24lPRPRGus
d9S6jr5N4qut+vSCp37NABijZj4uJ540nTH0R7qtuhTnynl4Q0/1wwiYvTvVF1Lg
dn+I/8aRbshwDhdAOWOUe6GL7/eaCYgN8/UmlKIpp8tg0w2iWxbaFiR7gZiM41LA
M6SXXfcCas+ZVXsGbzQ3SNiVurCGuuRNcCScXS3/WoEDIb3cNtp49iOmQS+nmEoo
wh4uiEd+0+BrzxngS4o5+mKnHJnwgY0+veGVYLMR5QKBgQD9WKQmevMDU5c+NPq9
8jaR457Fuxq1gwzeFNJdWfOc/K2LEWh+nFNFCb++EboEj6FdxWaWNMxbrmJps5gs
EoBUYy/Tl7UycDqDfiYLmDdTsf2pVjjh9jaIADiLcJ8S6wwJMZKub7Tp8UVkenAl
535MqShLUC11Y7VxLb3Tsll4XwKBgQD67mm6iCmshr/eszPfNE3ylZ+PiNa7nat7
N7lQzBIiRJflT1kmVidC5gE+jASqH728ChkZZKxbHsjxpmWdAhLOITdXoTB4sDsd
wtV1lxkXxK9FnrpFvO3y1wZ/QsD3Z2KXxHYZqawkUETO9F3nqAXW0b2GDar5Qiyo
J3Tx/43aHwKBgDC0NMJtCoDONhowZy/S+6iqQKC0qprQec3L5PErVMkOTnKYwyTr
+pogGKt6ju9HiXcUdvdTaSIK8UJu00dNuzv94XjlBmGO78DNpJTAC4rcge5m9AKE
qdEVcclkukARzbuKuy8rrHT4/CUn4J141m/4aRWpcUPLCluato6XD9ozAoGBANvf
JhOFFgcPd3YazfvpZ9eE1XA+tfFlYYmxNRcgCU+vjO0oDvSxjutmgHae18N91pG6
w21lskSRf/+GDwl5dKLbphOJsOA/gz07qDDGOf2CoRW+1Hcg6drcINxH0K+4DkLv
qZApBSY4k2JH6zR+HMeztn6M4WBRZLHfCPC3PUN/AoGAA3AoHbLTZvqMIKSDkP4Y
U/tTsSFDY4aYo7LG/jk8af3oPU3KyGh4ZFBd6aMmXbS8f8FjvmrM+/e+y9OOGAlq
iOl0hYrs5cJSMLW6i4KnJYuYbMkgmk3bN2t9apu64xKR94gbPrI6AGnPZp+iIzp0
hXKe4HcuhQ3G0a2hjayiQ84=
-----END PRIVATE KEY-----
@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+FSBWCRTALNeYyd1dyIn
B3e7CNkyBak2TBONM5TpVQJTyi2Zk0L2RSzYGlAALLz4uYtQ0dssMCugJQX58FZ8
75g1wuZihzs+KMTkFQOjZadslWkDM4nZn2DUpDLXL/+B0T7GrXOyvzOZeqQj9tA1
UY5IpAXSMvHvcPsegayBaeUPD2XP19gRjQFDDGAAnXmB+UfwNAsapDQ9d3rHOIJM
pzDq72a2Rhws+dbrH0gqsg1lsLDfhhui2FomuBpDZUtHq0Jz/IEvd3X45XvegSH8
t8+yL/pFK3+YpDVtj/IzMSwL+izvnXFALvZOO+8CABeyKuSjLh/6LbAzrvoftql5
gQIDAQAB
-----END PUBLIC KEY-----
-38
View File
@@ -1,38 +0,0 @@
# Module `guid`
## Introduction
Module `guid` serves as a guid creator for other `JDevKit` modules. You can also use this module as a guid creator standards.
We have already implemented `SnowflakeGuidCreator`, you can also implement a custom guid creations by implementing `com.onixbyte.identitygenerator.IdentityGenerator`.
## Example usage
### A UUID creator
```java
GuidCreator<UUID> uuidCreator = (GuidCreator<UUID>) UUID::randomUUID;
```
### A custom guid creator
Assume that you need serial guid creator.
```java
@Component
public class CustomGuidCreator implementes GuidCreator<String> {
public final RedisTemplate<String, Long> serialRedisTemplate;
@Autowired
public CustomGuidCreator(RedisTemplate<String, Long> serialRedisTemplate) {
this.serialRedisTemplate = serialRedisTemplate;
}
@Override public String nextId() {
return "SOME_PREFIX" + serialRedisTemplate.opsForValue().get("some_serial_key");
}
}
```
-131
View File
@@ -1,131 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import java.net.URI
plugins {
java
id("java-library")
id("maven-publish")
id("signing")
}
val identityGeneratorVersion: String by project
version = identityGeneratorVersion
val projectUrl: String by project
val projectGithubUrl: String by project
val licenseName: String by project
val licenseUrl: String by project
repositories {
mavenCentral()
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
withSourcesJar()
withJavadocJar()
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
tasks.withType<Jar> {
exclude("logback.xml")
}
dependencies {
compileOnly(libs.slf4j)
implementation(libs.logback)
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
}
tasks.test {
useJUnitPlatform()
}
publishing {
publications {
create<MavenPublication>("identityGenerator") {
groupId = group.toString()
artifactId = "identity-generator"
version = identityGeneratorVersion
pom {
name = "OnixByte Identity Generator"
description = "The module for generating GUIDs of JDevKit."
url = projectUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
scm {
connection = "scm:git:git://github.com:OnixByte/JDevKit.git"
developerConnection = "scm:git:git://github.com:OnixByte/JDevKit.git"
url = projectGithubUrl
}
developers {
developer {
id = "zihluwang"
name = "Zihlu Wang"
email = "really@zihlu.wang"
timezone = "Asia/Hong_Kong"
}
developer {
id = "siujamo"
name = "Siu Jam'o"
email = "jamo.siu@outlook.com"
timezone = "Asia/Shanghai"
}
}
}
from(components["java"])
signing {
setRequired(project.hasProperty("signing.keyId"))
sign(publishing.publications["identityGenerator"])
}
}
repositories {
maven {
name = "sonatypeNexus"
url = URI(providers.gradleProperty("repo.maven-central.host").get())
credentials {
username = providers.gradleProperty("repo.maven-central.username").get()
password = providers.gradleProperty("repo.maven-central.password").get()
}
}
}
}
}
@@ -1,65 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.identitygenerator;
/**
* The {@code IdentityGenerator} is a generic interface for generating globally unique identifiers
* (GUIDs) of a specific type.
* <p>
* The type of ID is determined by the class implementing this interface.
* </p>
*
* <p><b>Example usage:</b></p>
* <pre>{@code
* public class StringGuidCreator implements IdentityGenerator<String> {
* private final AtomicLong counter = new AtomicLong();
*
* @Override
* public String nextId() {
* return UUID.randomUUID().toString() + "-" + counter.incrementAndGet();
* }
* }
*
* public class Example {
* public static void main(String[] args) {
* IdentityGenerator<String> guidCreator = new StringGuidCreator();
* String guid = guidCreator.nextId();
* System.out.println("Generated GUID: " + guid);
* }
* }
* }</pre>
*
* @param <IdType> this represents the type of the Global Unique Identifier
* @author zihluwang
* @version 3.0.0
*/
public interface IdentityGenerator<IdType> {
/**
* Generates and returns the next globally unique ID.
*
* @return the next globally unique ID
*/
IdType nextId();
}
@@ -1,74 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.identitygenerator.exceptions;
/**
* The {@code TimingException} class represents an exception that is thrown when there is an error
* related to time sequence.
* <p>
* Instances of TimingException can be created with or without a message and a cause. The message
* provides a description of the exception, while the cause represents the underlying cause of the
* exception and provides additional information about the error.
*
* @author zihluwang
* @since 3.0.0
*/
public class TimingException extends RuntimeException {
/**
* A custom exception that is thrown when there is an issue with timing or scheduling.
*/
public TimingException() {
}
/**
* A custom exception that is thrown when there is an issue with timing or scheduling with
* customised error message.
*
* @param message customised message
*/
public TimingException(String message) {
super(message);
}
/**
* A custom exception that is thrown when there is an issue with timing or scheduling with
* customised error message.
*
* @param message customised message
* @param cause the cause of this exception
*/
public TimingException(String message, Throwable cause) {
super(message, cause);
}
/**
* A custom exception that is thrown when there is an issue with timing or scheduling with
* customised error message.
*
* @param cause the cause of this exception
*/
public TimingException(Throwable cause) {
super(cause);
}
}
@@ -1,95 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.identitygenerator.impl;
import com.onixbyte.identitygenerator.IdentityGenerator;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.UUID;
/**
* A {@code SequentialUuidGenerator} is responsible for generating UUIDs following the
* UUID version 7 specification, which combines a timestamp with random bytes to create time-ordered
* unique identifiers.
* <p>
* This implementation utilises a cryptographically strong {@link SecureRandom} instance to produce
* the random component of the UUID. The first 6 bytes of the UUID encode the current timestamp in
* milliseconds, ensuring that generated UUIDs are roughly ordered by creation time.
* <p>
* The generated UUID adheres strictly to the layout and variant bits of UUID version 7 as defined
* in the specification.
*
* @author zihluwang
* @author siujamo
* @version 3.0.0
*/
public class SequentialUuidGenerator implements IdentityGenerator<UUID> {
private final SecureRandom random;
/**
* Constructs a new {@code SequentialUuidGenerator} with its own {@link SecureRandom} instance.
*/
public SequentialUuidGenerator() {
this.random = new SecureRandom();
}
/**
* Generates and returns the next UUID version 7 identifier.
*
* @return a {@link UUID} instance representing a unique, time-ordered identifier
*/
@Override
public UUID nextId() {
var value = randomBytes();
var buf = ByteBuffer.wrap(value);
var high = buf.getLong();
var low = buf.getLong();
return new UUID(high, low);
}
/**
* Produces a byte array representation of a UUID version 7,
* combining the current timestamp with random bytes.
*
* @return a 16-byte array conforming to UUIDv7 layout and variant bits
*/
private byte[] randomBytes() {
var value = new byte[16];
random.nextBytes(value);
var timestamp = ByteBuffer.allocate(Long.BYTES);
timestamp.putLong(System.currentTimeMillis());
System.arraycopy(timestamp.array(), 2, value, 0, 6);
// Set version to 7 (UUIDv7)
value[6] = (byte) ((value[6] & 0x0F) | 0x70);
// Set variant bits as per RFC 4122
value[8] = (byte) ((value[8] & 0x3F) | 0x80);
return value;
}
}
@@ -1,207 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.identitygenerator.impl;
import com.onixbyte.identitygenerator.IdentityGenerator;
import com.onixbyte.identitygenerator.exceptions.TimingException;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* The {@code SnowflakeIdentityGenerator} generates unique identifiers using the
* Snowflake algorithm, which combines a timestamp, worker ID, and data centre ID to create 64-bit
* long integers. The bit distribution for the generated IDs is as follows:
* <ul>
* <li>1 bit for sign</li>
* <li>41 bits for timestamp (in milliseconds)</li>
* <li>5 bits for data centre ID</li>
* <li>5 bits for worker ID</li>
* <li>12 bits for sequence number (per millisecond)</li>
* </ul>
* <p>
* When initialising a {@link SnowflakeIdentityGenerator}, you must provide the worker ID and data
* centre ID, ensuring they are within the valid range defined by the bit size. The generator
* maintains an internal sequence number that increments for IDs generated within the
* same millisecond. If the system clock moves backward, an exception is thrown to prevent
* generating IDs with repeated timestamps.
*
* @author zihluwang
* @version 3.0.0
*/
public final class SnowflakeIdentityGenerator implements IdentityGenerator<Long> {
/**
* Default custom epoch.
*
* @value 2015-01-01T00:00:00Z
*/
private static final long DEFAULT_CUSTOM_EPOCH = 1_420_070_400_000L;
/**
* The start epoch timestamp to generate IDs from.
*/
private final long startEpoch;
/**
* The number of bits reserved for the data centre ID.
*/
private static final long DATA_CENTRE_ID_BITS = 5L;
/**
* The number of bits reserved for the worker ID.
*/
private static final long WORKER_ID_BITS = 5L;
/**
* The worker ID assigned to this generator.
*/
private final long workerId;
/**
* The data centre ID assigned to this generator.
*/
private final long dataCentreId;
/**
* The current sequence number.
*/
private long sequence = 0L;
/**
* The timestamp of the last generated ID.
*/
private long lastTimestamp = -1L;
/**
* Constructs a SnowflakeGuidGenerator with the default start epoch and custom worker ID, data
* centre ID.
*
* @param dataCentreId the data centre ID (between 0 and 31)
* @param workerId the worker ID (between 0 and 31)
*/
public SnowflakeIdentityGenerator(long dataCentreId, long workerId) {
this(dataCentreId, workerId, DEFAULT_CUSTOM_EPOCH);
}
/**
* Constructs a SnowflakeGuidGenerator with a custom epoch, worker ID, and data centre ID.
*
* @param dataCentreId the data centre ID (between 0 and 31)
* @param workerId the worker ID (between 0 and 31)
* @param startEpoch the custom epoch timestamp (in milliseconds) to start generating IDs from
* @throws IllegalArgumentException if the start epoch is greater than the current timestamp,
* or if the worker ID or data centre ID is out of range
*/
public SnowflakeIdentityGenerator(long dataCentreId, long workerId, long startEpoch) {
if (startEpoch > currentTimestamp()) {
throw new IllegalArgumentException("Start Epoch can not be greater than current timestamp!");
}
var maxWorkerId = ~(-1L << WORKER_ID_BITS);
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("Worker Id can't be greater than %d or less than 0",
maxWorkerId));
}
var maxDataCentreId = ~(-1L << DATA_CENTRE_ID_BITS);
if (dataCentreId > maxDataCentreId || dataCentreId < 0) {
throw new IllegalArgumentException(String.format("Data Centre Id can't be greater than %d or less than 0",
maxDataCentreId));
}
this.startEpoch = startEpoch;
this.workerId = workerId;
this.dataCentreId = dataCentreId;
}
/**
* Generates the next unique ID.
*
* @return the generated unique ID
* @throws TimingException if the system clock moves backwards, indicating an invalid sequence
* of timestamps.
*/
@Override
public synchronized Long nextId() {
var timestamp = currentTimestamp();
// if the current time is less than the timestamp of the last ID generation, it means that
// the system clock has been set back and an exception should be thrown
if (timestamp < lastTimestamp) {
throw new TimingException("Clock moved backwards. Refusing to generate id for %d milliseconds"
.formatted(lastTimestamp - timestamp));
}
// if generated at the same time, perform intra-millisecond sequences
var sequenceBits = 12L;
if (lastTimestamp == timestamp) {
var sequenceMask = ~(-1L << sequenceBits);
sequence = (sequence + 1) & sequenceMask;
// sequence overflow in milliseconds
if (sequence == 0) {
// block to the next millisecond, get a new timestamp
timestamp = awaitToNextMillis(lastTimestamp);
}
} else {
// timestamp change, sequence reset in milliseconds
sequence = 0L;
}
// timestamp of last ID generation
lastTimestamp = timestamp;
// shifted and put together by or operations to form a 64-bit ID
var timestampLeftShift = sequenceBits + WORKER_ID_BITS + DATA_CENTRE_ID_BITS;
var dataCentreIdShift = sequenceBits + WORKER_ID_BITS;
return ((timestamp - startEpoch) << timestampLeftShift)
| (dataCentreId << dataCentreIdShift)
| (workerId << sequenceBits)
| sequence;
}
/**
* Blocks until the next millisecond to obtain a new timestamp.
*
* @param lastTimestamp the timestamp when the last ID was generated
* @return the current timestamp
*/
private long awaitToNextMillis(long lastTimestamp) {
var timestamp = currentTimestamp();
while (timestamp <= lastTimestamp) {
timestamp = currentTimestamp();
}
return timestamp;
}
/**
* Returns the current timestamp in milliseconds.
*
* @return the current timestamp
*/
private long currentTimestamp() {
return LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
}
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2024-2026 OnixByte
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<configuration>
<property name="COLOURFUL_OUTPUT"
value="%black(%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK}) %highlight(%-5level) %black(---) %black([%10.10t]) %cyan(%-20.20logger{20}) %black(:) %msg%n"/>
<property name="STANDARD_OUTPUT"
value="%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK} %-5level %black(---) [%10.10t] %-20.20logger{20} : %msg%n"/>
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${COLOURFUL_OUTPUT}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
-4
View File
@@ -1,4 +0,0 @@
# Math Toolkit
**Math Toolkit** provides some mathematical algorithms and utilities such as chained high-precision
mathematical calculator and percentile statistic algorithm.
-132
View File
@@ -1,132 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import java.net.URI
plugins {
java
id("java-library")
id("maven-publish")
id("signing")
}
val mathToolboxVersion: String by project
version = mathToolboxVersion
val projectUrl: String by project
val projectGithubUrl: String by project
val licenseName: String by project
val licenseUrl: String by project
repositories {
mavenCentral()
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
withSourcesJar()
withJavadocJar()
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
tasks.withType<Jar> {
exclude("logback.xml")
}
dependencies {
compileOnly(libs.slf4j)
implementation(libs.logback)
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
}
tasks.test {
useJUnitPlatform()
}
publishing {
publications {
create<MavenPublication>("mathToolbox") {
groupId = group.toString()
artifactId = "math-toolbox"
version = mathToolboxVersion
pom {
name = "OnixByte Math Toolbox"
description =
"This module is an easy-to-use util for mathematical calculations in Java."
url = projectUrl
licenses {
license {
name = licenseName
url = licenseUrl
}
}
scm {
connection = "scm:git:git://github.com:onixbyte/onixbyte-toolbox.git"
developerConnection = "scm:git:git://github.com:onixbyte/onixbyte-toolbox.git"
url = projectGithubUrl
}
developers {
developer {
id = "zihluwang"
name = "Zihlu Wang"
email = "really@zihlu.wang"
timezone = "Asia/Hong_Kong"
}
developer {
id = "siujamo"
name = "Siu Jam'o"
email = "jamo.siu@outlook.com"
timezone = "Asia/Shanghai"
}
}
}
from(components["java"])
signing {
setRequired(project.hasProperty("signing.keyId"))
sign(publishing.publications["mathToolbox"])
}
}
repositories {
maven {
name = "sonatypeNexus"
url = URI(providers.gradleProperty("repo.maven-central.host").get())
credentials {
username = providers.gradleProperty("repo.maven-central.username").get()
password = providers.gradleProperty("repo.maven-central.password").get()
}
}
}
}
}
@@ -1,381 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.math;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* The {@code ChainedCalcUtil} class provides a convenient way to perform chained high-precision
* calculations using {@link BigDecimal}. It allows users to perform mathematical operations such
* as addition, subtraction, multiplication, and division with customisable precision and scale.
* By using this utility class, developers can achieve accurate results and avoid precision loss
* in their calculations.
* <p>
* <b>Usage:</b>
* <pre>
* // Perform addition: 3 + 4
* BigDecimal result1 = Calculator.startWith(3)
* .add(4)
* .getValue();
*
* // Perform subtraction: 4 - 2
* BigDecimal result2 = Calculator.startWith(4)
* .subtract(2)
* .getValue();
*
* // Perform multiplication: 3 * 6
* BigDecimal result3 = Calculator.startWith(3)
* .multiply(6)
* .getValue();
*
* // Perform division: 6 ÷ 2
* BigDecimal result4 = Calculator.startWith(6)
* .divide(2)
* .getValue();
*
* // Perform division with specified scale: 13 ÷ 7 with a scale of 2
* BigDecimal result5 = Calculator.startWith(13)
* .divideWithScale(7, 2)
* .getValue();
*
* // Get int, long, or double results
* int intResult = Calculator.startWith(3)
* .add(4)
* .getInteger();
*
* long longResult = Calculator.startWith(4)
* .subtract(2)
* .getLong();
*
* double doubleResult = Calculator.startWith(6)
* .divide(2)
* .getDouble();
*
* // Get BigDecimal result with specified scale
* BigDecimal result6 = Calculator.startWith(13)
* .divide(7)
* .getValue(2);
* </pre>
* The above expressions perform various mathematical calculations using the
* {@code ChainedCalcUtil} class.
* <p>
* <b>Note:</b>
* The {@code ChainedCalcUtil} class internally uses {@link BigDecimal} to handle high-precision
* calculations. It is important to note that {@link BigDecimal} operations can be memory-intensive
* and may have performance implications for extremely large numbers or complex calculations.
*
* @author sunzsh
* @version 3.3.0
* @see BigDecimal
* @since 1.0.0
*/
public final class Calculator {
/**
* Creates a {@code ChainedCalcUtil} instance with the specified initial value.
*
* @param value the initial value for the calculation
*/
private Calculator(Number value) {
this.value = convertBigDecimal(value, null);
}
/**
* Starts a chained calculation with the specified initial value.
*
* @param value the initial value for the calculation
* @return a {@code ChainedCalcUtil} instance for performing chained calculations
*/
public static Calculator startWith(Number value) {
return new Calculator(value);
}
/**
* Adds the specified value to the current value.
*
* @param other the value to be added
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator add(Number other) {
return operator(BigDecimal::add, other);
}
/**
* Adds the specified value to the current value with a specified scale before the operation.
*
* @param other the value to be added
* @param beforeOperateScale the scale to be applied before the operation
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator add(Number other, Integer beforeOperateScale) {
return operator(BigDecimal::add, other, beforeOperateScale);
}
/**
* Subtracts the specified value from the current value.
*
* @param other the value to be subtracted
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator subtract(Number other) {
return operator(BigDecimal::subtract, other);
}
/**
* Subtracts the specified value from the current value with a specified scale before
* the operation.
*
* @param other the value to be subtracted
* @param beforeOperateScale the scale to be applied before the operation
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator subtract(Number other, Integer beforeOperateScale) {
return operator(BigDecimal::subtract, other, beforeOperateScale);
}
/**
* Multiplies the current value by the specified value.
*
* @param other the value to be multiplied by
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator multiply(Number other) {
return operator(BigDecimal::multiply, other);
}
/**
* Multiplies the current value by the specified value with a specified scale before
* the operation.
*
* @param other the value to be multiplied by
* @param beforeOperateScale the scale to be applied before the operation
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator multiply(Number other, Integer beforeOperateScale) {
return operator(BigDecimal::multiply, other, beforeOperateScale);
}
/**
* Divides the current value by the specified value.
*
* @param other the value to divide by
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator divide(Number other) {
return operator(BigDecimal::divide, other);
}
/**
* Divides the current value by the specified value with a specified scale before the operation.
*
* @param other the value to divide by
* @param beforeOperateScale the scale to be applied before the operation
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator divide(Number other, Integer beforeOperateScale) {
return operator(BigDecimal::divide, other, beforeOperateScale);
}
/**
* Divides the current value by the specified value with a specified scale.
*
* @param other the value to divide by
* @param scale the scale for the result
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator divideWithScale(Number other, Integer scale) {
return baseOperator(otherValue ->
this.value.divide(otherValue, scale, RoundingMode.HALF_UP), other, null);
}
/**
* Divides the current value by the specified value with a specified scale and a scale applied
* before the operation.
*
* @param other the value to divide by
* @param scale the scale for the result
* @param beforeOperateScale the scale to be applied before the operation
* @return a {@code ChainedCalcUtil} instance with the updated value
*/
public Calculator divideWithScale(Number other, Integer scale, Integer beforeOperateScale) {
return baseOperator((otherValue) ->
this.value.divide(otherValue, scale, RoundingMode.HALF_UP),
other, beforeOperateScale);
}
/**
* Returns the current value as a {@link BigDecimal} with the specified scale.
*
* @param scale the scale for the result
* @return the current value as a {@link BigDecimal} with the specified scale
*/
public BigDecimal getValue(int scale) {
return value.setScale(scale, RoundingMode.HALF_UP);
}
/**
* Returns the current value as a {@link BigDecimal}.
*
* @return the current value as a {@link BigDecimal}
*/
public BigDecimal getValue() {
return value;
}
/**
* Returns the current value as a {@link Double}.
*
* @return the current value as a {@link Double}
*/
public double getDouble() {
return getValue().doubleValue();
}
/**
* Returns the current value as a {@link Double} with the specified scale.
*
* @param scale the scale for the result
* @return the current value as a {@link Double} with the specified scale
*/
public double getDouble(int scale) {
return getValue(scale).doubleValue();
}
/**
* Returns the current value as a {@link Long}.
*
* @return the current value as a {@link Long}
*/
public long getLong() {
return getValue().longValue();
}
/**
* Returns the current value as an {@link Integer}.
*
* @return the current value as an {@link Integer}
*/
public int getInteger() {
return getValue().intValue();
}
/**
* Applies the specified operator function to the current value and another value.
*
* @param operator the operator function to apply
* @param otherValue the value to apply the operator with
* @return a ChainedCalcUtil instance with the updated value
*/
private Calculator operator(
BiFunction<BigDecimal, BigDecimal, BigDecimal> operator,
Object otherValue
) {
return operator(operator, otherValue, 9);
}
/**
* Applies the specified operator function to the current value and another value with a
* specified scale before the operation.
*
* @param operator the operator function to apply
* @param other the value to apply the operator with
* @param beforeOperateScale the scale to be applied before the operation,
* or null if not applicable
* @return a ChainedCalcUtil instance with the updated value
*/
private Calculator operator(
BiFunction<BigDecimal, BigDecimal, BigDecimal> operator,
Object other,
Integer beforeOperateScale
) {
return baseOperator(
(otherValue) -> operator.apply(this.value, otherValue),
other,
beforeOperateScale
);
}
/**
* Applies the specified operator function to the current value and another
* value.
*
* @param operatorFunction the operator function to apply
* @param anotherValue the value to apply the operator with
* @param beforeOperateScale the scale to be applied before the operation,
* or null if not applicable
* @return a ChainedCalcUtil instance with the updated value
*/
private synchronized Calculator baseOperator(
Function<BigDecimal, BigDecimal> operatorFunction,
Object anotherValue,
Integer beforeOperateScale
) {
if (Objects.isNull(anotherValue)) {
return this;
}
if (anotherValue instanceof Calculator) {
this.value = operatorFunction.apply(((Calculator) anotherValue).getValue());
return this;
}
this.value = operatorFunction.apply(convertBigDecimal(anotherValue, beforeOperateScale));
return this;
}
/**
* Converts the specified value to a {@link BigDecimal}.
*
* @param value the value to convert
* @param scale the scale to apply to the resulting BigDecimal, or null if not applicable
* @return the converted BigDecimal value
*/
private BigDecimal convertBigDecimal(Object value, Integer scale) {
if (Objects.isNull(value)) {
return BigDecimal.ZERO;
}
BigDecimal res;
if (value instanceof Number num) {
res = BigDecimal.valueOf(num.doubleValue());
} else {
try {
res = new BigDecimal(value.toString());
} catch (NumberFormatException ignored) {
return BigDecimal.ZERO;
}
}
if (Objects.nonNull(scale)) {
return res.setScale(scale, RoundingMode.HALF_UP);
}
return res;
}
/**
* Final result.
*/
private BigDecimal value;
}
@@ -1,120 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.math;
import com.onixbyte.math.model.QuartileBounds;
import java.util.List;
/**
* A utility class that provides methods for calculating percentiles and interquartile range (IQR)
* bounds for a dataset.
* <p>
* This class contains static methods to:
* <ul>
* <li>
* Calculate a specified percentile from a list of double values using linear interpolation.
* </li>
* <li>
* Calculate interquartile bounds (Q1, Q3) and the corresponding lower and upper bounds,
* which can be used to identify outliers in the dataset.
* </li>
* </ul>
* <p>
* This class is final, meaning it cannot be subclassed, and it only contains static methods,
* so instances of the class cannot be created.
* <h2>Example usage:</h2>
* <pre>{@code
* List<Double> data = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
* Double percentileValue = PercentileCalculator.calculatePercentile(data, 50.0); // Calculates median
* QuartileBounds bounds = PercentileCalculator.calculatePercentileBounds(data); // Calculates IQR bounds
* }</pre>
*
* @author zihluwang
* @version 3.0.0
* @since 1.6.5
*/
public final class PercentileCalculator {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private PercentileCalculator() {
}
/**
* Calculates the specified percentile from a list of values.
* <p>
* This method takes a list of double values and calculates the given percentile using linear
* interpolation between the two closest ranks. The list is first sorted in ascending order,
* and the specified percentile is then calculated.
*
* @param values a list of {@code Double} values from which the percentile is calculated.
* @param percentile a {@code Double} representing the percentile to be calculated (e.g., 50.0
* for the median)
* @return a {@code Double} value representing the calculated percentile
*/
public static Double calculatePercentile(List<Double> values, Double percentile) {
if (values.isEmpty()) {
throw new IllegalArgumentException("Unable to sort an empty list.");
}
var sorted = values.stream().sorted().toList();
var rank = percentile / 100. * (sorted.size() - 1);
var lowerIndex = (int) Math.floor(rank);
var upperIndex = (int) Math.ceil(rank);
var weight = rank - lowerIndex;
return sorted.get(lowerIndex) * (1 - weight) + sorted.get(upperIndex) * weight;
}
/**
* Calculates the interquartile range (IQR) and the corresponding lower and upper bounds
* based on the first (Q1) and third (Q3) quartiles of a dataset.
* <p>
* This method takes a list of double values, calculates the first quartile (Q1),
* the third quartile (Q3), and the interquartile range (IQR). Using the IQR, it computes
* the lower and upper bounds, which can be used to detect outliers in the dataset.
* The lower bound is defined as {@code Q1 - 1.5 * IQR}, and the upper bound is defined as
* {@code Q3 + 1.5 * IQR}.
*
* @param data a list of {@code Double} values for which the quartile bounds will be calculated
* @return a {@code QuartileBounds} object containing the calculated lower and upper bounds
*/
public static QuartileBounds calculatePercentileBounds(List<Double> data) {
var sorted = data.stream().sorted().toList();
var q1 = calculatePercentile(sorted, 25.);
var q3 = calculatePercentile(sorted, 75.);
var iqr = q3 - q1;
var lowerBound = q1 - 1.5 * iqr;
var upperBound = q3 + 1.5 * iqr;
return QuartileBounds.builder()
.upperBound(upperBound)
.lowerBound(lowerBound)
.build();
}
}
@@ -1,136 +0,0 @@
/*
* Copyright (c) 2024-2026 OnixByte
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.onixbyte.math.model;
/**
* A record representing the quartile bounds of a dataset.
* <p>
* This class encapsulates the lower and upper bounds of a dataset, which are typically used for
* detecting outliers in the data. The bounds are calculated based on the interquartile range (IQR)
* of the dataset. Values below the lower bound or above the upper bound may be considered outliers.
* <p>
* Quartile bounds consist of:
* <ul>
* <li>
* {@code lowerBound} - The lower bound of the dataset, typically {@code Q1 - 1.5 * IQR}.
* </li>
* <li>
* {@code upperBound} - The upper bound of the dataset, typically {@code Q3 + 1.5 * IQR}.
* </li>
* </ul>
* <p>
* Example usage:
* <pre>{@code
* QuartileBounds bounds = QuartileBounds.builder()
* .lowerBound(1.5)
* .upperBound(7.5)
* .build();
* }</pre>
*
* @param upperBound the upper bound of the dataset
* @param lowerBound the lower bound of the dataset
* @author zihluwang
* @version 3.0.0
* @since 1.6.5
*/
public record QuartileBounds(
Double upperBound,
Double lowerBound
) {
/**
* Creates a new {@link Builder} instance for building a {@code QuartileBounds} object.
* <p>
* The {@link Builder} pattern is used to construct the {@code QuartileBounds} object with
* optional values for the upper and lower bounds.
* </p>
*
* @return a new instance of the {@link Builder} class
*/
public static Builder builder() {
return new Builder();
}
/**
* A builder class for constructing instances of the {@code QuartileBounds} record.
* <p>
* The {@link Builder} pattern allows for the step-by-step construction of a
* {@code QuartileBounds} object, providing a flexible way to set values for the lower and
* upper bounds. Once the builder has the required values, the {@link #build()} method creates
* and returns a new {@code QuartileBounds} object.
* </p>
* <p>
* Example usage:
* <pre>
* {@code
* QuartileBounds bounds = QuartileBounds.builder()
* .lowerBound(1.5)
* .upperBound(7.5)
* .build();
* }
* </pre>
*/
public static class Builder {
private Double upperBound;
private Double lowerBound;
/**
* Private constructor to prevent instantiation of this utility class.
*/
private Builder() {
}
/**
* Sets the upper bound for the {@code QuartileBounds}.
*
* @param upperBound the upper bound of the dataset
* @return the current {@code Builder} instance, for method chaining
*/
public Builder upperBound(Double upperBound) {
this.upperBound = upperBound;
return this;
}
/**
* Sets the lower bound for the {@code QuartileBounds}.
*
* @param lowerBound the lower bound of the dataset
* @return the current {@code Builder} instance, for method chaining
*/
public Builder lowerBound(Double lowerBound) {
this.lowerBound = lowerBound;
return this;
}
/**
* Builds and returns a new {@code QuartileBounds} instance with the specified upper and
* lower bounds.
*
* @return a new {@code QuartileBounds} object containing the specified bounds
*/
public QuartileBounds build() {
return new QuartileBounds(upperBound, lowerBound);
}
}
}
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2024-2026 OnixByte
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
-->
<configuration>
<property name="COLOURFUL_OUTPUT"
value="%black(%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK}) %highlight(%-5level) %black(---) %black([%10.10t]) %cyan(%-20.20logger{20}) %black(:) %msg%n"/>
<property name="STANDARD_OUTPUT"
value="%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK} %-5level %black(---) [%10.10t] %-20.20logger{20} : %msg%n"/>
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${COLOURFUL_OUTPUT}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>