feat(database): Add Redis support
This commit is contained in:
parent
c9453cb1b3
commit
5e7f42b76e
12
pom.xml
12
pom.xml
@ -182,6 +182,12 @@
|
||||
<version>32.1.3-jre</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Databases -->
|
||||
<dependency>
|
||||
@ -190,5 +196,11 @@
|
||||
<version>4.11.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.lettuce</groupId>
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
<version>6.3.0.RELEASE</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -1,5 +1,7 @@
|
||||
package me.braydon.feather;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@ -13,4 +15,11 @@ public final class FeatherSettings {
|
||||
* The amount of threads to use for {@link FeatherThreads}.
|
||||
*/
|
||||
@Setter @Getter private static int threadCount = 4;
|
||||
|
||||
/**
|
||||
* The {@link Gson} instance to use for serialization.
|
||||
*/
|
||||
@Setter @Getter private static Gson gson = new GsonBuilder()
|
||||
.serializeNulls()
|
||||
.create();
|
||||
}
|
@ -15,6 +15,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
public final class FeatherThreads {
|
||||
private static final AtomicInteger ID = new AtomicInteger(0); // The thread id
|
||||
public static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(FeatherSettings.getThreadCount(), new ThreadFactoryBuilder()
|
||||
.setNameFormat("Feather Thread #" + (ID.incrementAndGet()))
|
||||
.setNameFormat("Feather Thread #" + ID.incrementAndGet())
|
||||
.build()); // The thread pool to execute on
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package me.braydon.feather;
|
||||
|
||||
import lombok.NonNull;
|
||||
import me.braydon.feather.annotation.Collection;
|
||||
import me.braydon.feather.annotation.Field;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
@ -61,4 +63,16 @@ public interface IDatabase<B, C, S, A> extends Closeable {
|
||||
* @see A for asynchronous pipeline
|
||||
*/
|
||||
@NonNull A async();
|
||||
|
||||
/**
|
||||
* Write the given object to the database.
|
||||
* <p>
|
||||
* This object is an instance of a class
|
||||
* annotated with {@link Collection}, and
|
||||
* contains fields annotated with {@link Field}.
|
||||
* </p>
|
||||
*
|
||||
* @param element the element to write
|
||||
*/
|
||||
void write(@NonNull Object element);
|
||||
}
|
21
src/main/java/me/braydon/feather/annotation/Collection.java
Normal file
21
src/main/java/me/braydon/feather/annotation/Collection.java
Normal file
@ -0,0 +1,21 @@
|
||||
package me.braydon.feather.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Classes tagged with this annotation
|
||||
* will be treated as a collection.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented @Inherited
|
||||
public @interface Collection {
|
||||
/**
|
||||
* The name of this collection.
|
||||
*
|
||||
* @return the name
|
||||
*/
|
||||
String name() default "";
|
||||
}
|
25
src/main/java/me/braydon/feather/annotation/Field.java
Normal file
25
src/main/java/me/braydon/feather/annotation/Field.java
Normal file
@ -0,0 +1,25 @@
|
||||
package me.braydon.feather.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Fields tagged with this annotation will be
|
||||
* treated as a field within a {@link Collection}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD })
|
||||
@Documented @Inherited
|
||||
public @interface Field {
|
||||
/**
|
||||
* The key of this field.
|
||||
* <p>
|
||||
* If empty, the field name
|
||||
* will be used as the key.
|
||||
* </p>
|
||||
*
|
||||
* @return the key
|
||||
*/
|
||||
String key() default "";
|
||||
}
|
22
src/main/java/me/braydon/feather/annotation/Id.java
Normal file
22
src/main/java/me/braydon/feather/annotation/Id.java
Normal file
@ -0,0 +1,22 @@
|
||||
package me.braydon.feather.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Fields tagged with this annotation will be treated
|
||||
* as the primary identifying key for documents within
|
||||
* a {@link Collection}.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
@Documented @Inherited
|
||||
public @interface Id {
|
||||
/**
|
||||
* The key of this field.
|
||||
*
|
||||
* @return the key
|
||||
*/
|
||||
String key() default "_id";
|
||||
}
|
94
src/main/java/me/braydon/feather/data/Document.java
Normal file
94
src/main/java/me/braydon/feather/data/Document.java
Normal file
@ -0,0 +1,94 @@
|
||||
package me.braydon.feather.data;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
import me.braydon.feather.FeatherSettings;
|
||||
import me.braydon.feather.IDatabase;
|
||||
import me.braydon.feather.annotation.Collection;
|
||||
import me.braydon.feather.annotation.Field;
|
||||
import me.braydon.feather.annotation.Id;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A document is a key-value pair that is stored
|
||||
* within a {@link Collection}. This document is
|
||||
* based on the Bson {@link org.bson.Document}
|
||||
* in MongoDB, however this document is universal
|
||||
* between all {@link IDatabase}'s.
|
||||
*
|
||||
* @author Braydon
|
||||
* @param <V> the type of value this document holds
|
||||
*/
|
||||
@Getter @ToString
|
||||
public class Document<V> {
|
||||
/**
|
||||
* The key to use for the id field.
|
||||
*/
|
||||
@NonNull private final String idKey;
|
||||
|
||||
/**
|
||||
* The key of this document.
|
||||
*/
|
||||
@NonNull private final Object key;
|
||||
|
||||
/**
|
||||
* The mapped data of this document.
|
||||
*
|
||||
* @see V for value type
|
||||
*/
|
||||
private final Map<String, V> mappedData = Collections.synchronizedMap(new LinkedHashMap<>());
|
||||
|
||||
public Document(@NonNull Object element, boolean rawObject) {
|
||||
Class<?> clazz = element.getClass(); // Get the element class
|
||||
String idKey = null; // The key for the id field
|
||||
for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
|
||||
// Field is missing the @Field annotation, skip it
|
||||
if (!field.isAnnotationPresent(Field.class)) {
|
||||
continue;
|
||||
}
|
||||
field.setAccessible(true); // Make our field accessible
|
||||
boolean idField = field.isAnnotationPresent(Id.class); // Is this field annotated with @Id?
|
||||
Field annotation = field.getAnnotation(Field.class); // Get the @Field annotation
|
||||
String key = idField ? field.getAnnotation(Id.class).key() : annotation.key(); // The key of the database field
|
||||
if (key.isEmpty()) { // No field in the annotation, use the field name
|
||||
key = field.getName();
|
||||
}
|
||||
// The field is annotated with @Id, save it for later
|
||||
if (idField) {
|
||||
idKey = key;
|
||||
}
|
||||
Class<?> fieldType = field.getType(); // The type of the field
|
||||
try {
|
||||
Object value; // The value of the field
|
||||
|
||||
if (fieldType == UUID.class) { // Convert UUIDs into strings
|
||||
value = ((UUID) field.get(element)).toString();
|
||||
} else if (rawObject) { // Use the raw object from the field
|
||||
value = field.get(element);
|
||||
} else { // Otherwise, turn the value into a string
|
||||
if (fieldType == String.class) { // Already a string, cast it
|
||||
value = field.get(element);
|
||||
} else { // Convert the field into json using Gson
|
||||
value = FeatherSettings.getGson().toJson(field.get(element));
|
||||
}
|
||||
}
|
||||
mappedData.put(key, (V) value); // Store in our map
|
||||
} catch (IllegalAccessException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
assert idKey != null; // We need an id key
|
||||
this.idKey = idKey; // We have our id key
|
||||
|
||||
V key = mappedData.get(idKey); // Get the id from the data map
|
||||
if (key == null) { // The element is missing an id field
|
||||
throw new IllegalArgumentException("No @Id annotated field found in " + clazz.getSimpleName());
|
||||
}
|
||||
this.key = key;
|
||||
}
|
||||
}
|
@ -3,10 +3,16 @@ package me.braydon.feather.databases.mongodb;
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import com.mongodb.client.model.Filters;
|
||||
import com.mongodb.client.model.UpdateOptions;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import me.braydon.feather.IDatabase;
|
||||
import me.braydon.feather.annotation.Collection;
|
||||
import me.braydon.feather.annotation.Field;
|
||||
import me.braydon.feather.data.Document;
|
||||
|
||||
/**
|
||||
* The {@link IDatabase} implementation for MongoDB.
|
||||
@ -16,7 +22,7 @@ import me.braydon.feather.IDatabase;
|
||||
* @see ConnectionString for the credentials class
|
||||
* @see MongoSyncPipeline for the sync pipeline class
|
||||
* @see MongoAsyncPipeline for the async pipeline class
|
||||
* @see <a href="https://www.mongodb.com/">MongoDB Official Site</a>
|
||||
* @see <a href="https://www.mongodb.com">MongoDB Official Site</a>
|
||||
*/
|
||||
public class MongoDB implements IDatabase<MongoClient, ConnectionString, MongoSyncPipeline, MongoAsyncPipeline> {
|
||||
/**
|
||||
@ -109,6 +115,38 @@ public class MongoDB implements IDatabase<MongoClient, ConnectionString, MongoSy
|
||||
return new MongoAsyncPipeline(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the given object to the database.
|
||||
* <p>
|
||||
* This object is an instance of a class
|
||||
* annotated with {@link Collection}, and
|
||||
* contains fields annotated with {@link Field}.
|
||||
* </p>
|
||||
*
|
||||
* @param element the element to write
|
||||
*/
|
||||
@Override
|
||||
public void write(@NonNull Object element) {
|
||||
Class<?> clazz = element.getClass(); // Get the element class
|
||||
if (!clazz.isAnnotationPresent(Collection.class)) { // Missing annotation
|
||||
throw new IllegalStateException("Element is missing @Collection annotation");
|
||||
}
|
||||
Collection annotation = clazz.getAnnotation(Collection.class); // Get the @Collection annotation
|
||||
String collectionName = annotation.name(); // The name of the collection
|
||||
if (collectionName.isEmpty()) { // Missing collection name
|
||||
throw new IllegalStateException("Missing collection name in @Collection for " + clazz.getSimpleName());
|
||||
}
|
||||
MongoCollection<org.bson.Document> collection = database.getCollection(collectionName); // Get the collection
|
||||
Document<Object> document = new Document<>(element, true); // Construct the document from the element
|
||||
|
||||
// Set the map in the database
|
||||
collection.updateOne(
|
||||
Filters.eq(document.getIdKey(), document.getKey()),
|
||||
new org.bson.Document("$set", new org.bson.Document(document.getMappedData())),
|
||||
new UpdateOptions().upsert(true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this stream and releases any system resources associated
|
||||
* with it. If the stream is already closed then invoking this
|
||||
|
@ -6,7 +6,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
|
||||
/**
|
||||
* The pipeline for handling {@link MongoDB} operations.
|
||||
* The pipeline for handling synchronous {@link MongoDB} operations.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@ -14,6 +14,8 @@ import lombok.NonNull;
|
||||
public final class MongoSyncPipeline {
|
||||
/**
|
||||
* The database to handle operations for.
|
||||
*
|
||||
* @see MongoDB for database
|
||||
*/
|
||||
@NonNull private final MongoDB database;
|
||||
|
||||
|
149
src/main/java/me/braydon/feather/databases/redis/Redis.java
Normal file
149
src/main/java/me/braydon/feather/databases/redis/Redis.java
Normal file
@ -0,0 +1,149 @@
|
||||
package me.braydon.feather.databases.redis;
|
||||
|
||||
import io.lettuce.core.RedisClient;
|
||||
import io.lettuce.core.RedisURI;
|
||||
import io.lettuce.core.api.StatefulRedisConnection;
|
||||
import io.lettuce.core.api.async.RedisAsyncCommands;
|
||||
import io.lettuce.core.api.sync.RedisCommands;
|
||||
import lombok.NonNull;
|
||||
import me.braydon.feather.IDatabase;
|
||||
import me.braydon.feather.annotation.Collection;
|
||||
import me.braydon.feather.annotation.Field;
|
||||
import me.braydon.feather.data.Document;
|
||||
|
||||
/**
|
||||
* The {@link IDatabase} implementation for Redis.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see StatefulRedisConnection for the bootstrap class
|
||||
* @see RedisURI for the credentials class
|
||||
* @see RedisCommands for the sync pipeline class
|
||||
* @see RedisAsyncCommands for the async pipeline class
|
||||
* @see <a href="https://redis.io">Redis Official Site</a>
|
||||
*/
|
||||
public class Redis implements IDatabase<StatefulRedisConnection<String, String>, RedisURI, RedisCommands<String, String>, RedisAsyncCommands<String, String>> {
|
||||
/**
|
||||
* The current {@link RedisClient} instance.
|
||||
*/
|
||||
private RedisClient client;
|
||||
|
||||
/**
|
||||
* The current established {@link StatefulRedisConnection}.
|
||||
*/
|
||||
private StatefulRedisConnection<String, String> connection;
|
||||
|
||||
/**
|
||||
* Get the name of this database.
|
||||
*
|
||||
* @return the database name
|
||||
*/
|
||||
@Override @NonNull
|
||||
public String getName() {
|
||||
return "Redis";
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a connection to this database.
|
||||
*
|
||||
* @param credentials the optional credentials to use
|
||||
*/
|
||||
@Override
|
||||
public void connect(RedisURI credentials) {
|
||||
if (credentials == null) { // We need valid credentials
|
||||
throw new IllegalArgumentException("No credentials defined");
|
||||
}
|
||||
if (isConnected()) { // Already connected
|
||||
throw new IllegalStateException("Already connected");
|
||||
}
|
||||
if (client != null) { // We have a client, close it first
|
||||
client.close();
|
||||
}
|
||||
if (connection != null) { // We have a connection, close it first
|
||||
connection.close();
|
||||
}
|
||||
client = RedisClient.create(credentials); // Create a new client
|
||||
connection = client.connect(); // Connect to the Redis server
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this database is connected.
|
||||
*
|
||||
* @return the database connection state
|
||||
*/
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return client != null && (connection != null && connection.isOpen());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bootstrap class
|
||||
* instance for this database.
|
||||
*
|
||||
* @return the bootstrap class instance, null if none
|
||||
* @see StatefulRedisConnection for bootstrap class
|
||||
*/
|
||||
@Override
|
||||
public StatefulRedisConnection<String, String> getBootstrap() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the synchronized
|
||||
* pipeline for this database.
|
||||
*
|
||||
* @return the synchronized pipeline
|
||||
* @see RedisPipeline for synchronized pipeline
|
||||
*/
|
||||
@Override @NonNull
|
||||
public RedisCommands<String, String> sync() {
|
||||
return connection.sync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the asynchronous
|
||||
* pipeline for this database.
|
||||
*
|
||||
* @return the asynchronous pipeline
|
||||
* @see RedisPipeline for asynchronous pipeline
|
||||
*/
|
||||
@Override @NonNull
|
||||
public RedisAsyncCommands<String, String> async() {
|
||||
return connection.async();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the given object to the database.
|
||||
* <p>
|
||||
* This object is an instance of a class
|
||||
* annotated with {@link Collection}, and
|
||||
* contains fields annotated with {@link Field}.
|
||||
* </p>
|
||||
*
|
||||
* @param element the element to write
|
||||
*/
|
||||
@Override
|
||||
public void write(@NonNull Object element) {
|
||||
if (!element.getClass().isAnnotationPresent(Collection.class)) { // Missing annotation
|
||||
throw new IllegalStateException("Element is missing @Collection annotation");
|
||||
}
|
||||
Document<String> document = new Document<>(element, false); // Construct the document from the element
|
||||
sync().hmset(String.valueOf(document.getKey()), document.getMappedData()); // Set the map in the database
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this stream and releases any system resources associated
|
||||
* with it. If the stream is already closed then invoking this
|
||||
* method has no effect.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (client != null) {
|
||||
client.close();
|
||||
}
|
||||
if (connection != null) {
|
||||
connection.close();
|
||||
}
|
||||
client = null;
|
||||
connection = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package me.braydon.feather.databases.redis;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
|
||||
/**
|
||||
* The pipeline for handling {@link Redis} operations.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class RedisPipeline {
|
||||
/**
|
||||
* The database to handle operations for.
|
||||
*
|
||||
* @see Redis for database
|
||||
*/
|
||||
@NonNull private final Redis database;
|
||||
}
|
23
src/main/java/me/braydon/feather/repository/Repository.java
Normal file
23
src/main/java/me/braydon/feather/repository/Repository.java
Normal file
@ -0,0 +1,23 @@
|
||||
package me.braydon.feather.repository;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import me.braydon.feather.IDatabase;
|
||||
|
||||
/**
|
||||
* A repository belonging to a {@link IDatabase}.
|
||||
*
|
||||
* @author Braydon
|
||||
* @param <D> the database
|
||||
*/
|
||||
@AllArgsConstructor @Getter(AccessLevel.PROTECTED)
|
||||
public abstract class Repository<D extends IDatabase<?, ?, ?, ?>> {
|
||||
/**
|
||||
* The database this repository belongs to.
|
||||
*
|
||||
* @see D for database
|
||||
*/
|
||||
@NonNull private final D database;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user