refactor: rename modules

Closes #75
This commit is contained in:
siujamo
2025-06-17 16:32:57 +08:00
parent 6793e90331
commit 71e7993352
79 changed files with 501 additions and 336 deletions
@@ -0,0 +1,61 @@
/*
* 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.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 Zihlu Wang
* @version 1.1.0
* @since 1.0.0
*/
public interface IdentityGenerator<IdType> {
/**
* Generates and returns the next globally unique ID.
*
* @return the next globally unique ID
*/
IdType nextId();
}
@@ -0,0 +1,69 @@
/*
* 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.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 Zihlu Wang
* @since 1.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);
}
}
@@ -0,0 +1,85 @@
/*
* 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.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.
* </p>
*/
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;
}
}
@@ -0,0 +1,204 @@
/*
* 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.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 initializing 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 Zihlu Wang
* @version 1.1.0
* @since 1.0.0
*/
public final class SnowflakeIdentityGenerator implements IdentityGenerator<Long> {
/**
* 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 << workerIdBits);
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 << dataCentreIdBits);
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
long sequenceBits = 12L;
if (lastTimestamp == timestamp) {
long 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);
}
}
// timestamp change, sequence reset in milliseconds
else {
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 + workerIdBits + dataCentreIdBits;
var dataCentreIdShift = sequenceBits + workerIdBits;
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();
}
/**
* 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 worker ID.
*/
private final long workerIdBits = 5L;
/**
* The number of bits reserved for the data centre ID.
*/
private final long dataCentreIdBits = 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;
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<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>