From 5e7f42b76e8b6a8a1bab8eae22a17f109464394b Mon Sep 17 00:00:00 2001
From: Braydon
Date: Tue, 12 Dec 2023 19:03:14 -0500
Subject: [PATCH] feat(database): Add Redis support
---
pom.xml | 12 ++
.../me/braydon/feather/FeatherSettings.java | 9 ++
.../me/braydon/feather/FeatherThreads.java | 2 +-
.../java/me/braydon/feather/IDatabase.java | 14 ++
.../feather/annotation/Collection.java | 21 +++
.../me/braydon/feather/annotation/Field.java | 25 +++
.../me/braydon/feather/annotation/Id.java | 22 +++
.../me/braydon/feather/data/Document.java | 94 +++++++++++
.../feather/databases/mongodb/MongoDB.java | 40 ++++-
.../databases/mongodb/MongoSyncPipeline.java | 4 +-
.../feather/databases/redis/Redis.java | 149 ++++++++++++++++++
.../databases/redis/RedisPipeline.java | 20 +++
.../feather/repository/Repository.java | 23 +++
13 files changed, 432 insertions(+), 3 deletions(-)
create mode 100644 src/main/java/me/braydon/feather/annotation/Collection.java
create mode 100644 src/main/java/me/braydon/feather/annotation/Field.java
create mode 100644 src/main/java/me/braydon/feather/annotation/Id.java
create mode 100644 src/main/java/me/braydon/feather/data/Document.java
create mode 100644 src/main/java/me/braydon/feather/databases/redis/Redis.java
create mode 100644 src/main/java/me/braydon/feather/databases/redis/RedisPipeline.java
create mode 100644 src/main/java/me/braydon/feather/repository/Repository.java
diff --git a/pom.xml b/pom.xml
index f57ea14..dde6155 100644
--- a/pom.xml
+++ b/pom.xml
@@ -182,6 +182,12 @@
32.1.3-jrecompile
+
+ com.google.code.gson
+ gson
+ 2.10.1
+ compile
+
@@ -190,5 +196,11 @@
4.11.1compile
+
+ io.lettuce
+ lettuce-core
+ 6.3.0.RELEASE
+ compile
+
\ No newline at end of file
diff --git a/src/main/java/me/braydon/feather/FeatherSettings.java b/src/main/java/me/braydon/feather/FeatherSettings.java
index 25b1d26..e263150 100644
--- a/src/main/java/me/braydon/feather/FeatherSettings.java
+++ b/src/main/java/me/braydon/feather/FeatherSettings.java
@@ -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();
}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/feather/FeatherThreads.java b/src/main/java/me/braydon/feather/FeatherThreads.java
index 7d3cbe0..8905bb0 100644
--- a/src/main/java/me/braydon/feather/FeatherThreads.java
+++ b/src/main/java/me/braydon/feather/FeatherThreads.java
@@ -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
}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/feather/IDatabase.java b/src/main/java/me/braydon/feather/IDatabase.java
index f6e9d05..d153c98 100644
--- a/src/main/java/me/braydon/feather/IDatabase.java
+++ b/src/main/java/me/braydon/feather/IDatabase.java
@@ -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 extends Closeable {
* @see A for asynchronous pipeline
*/
@NonNull A async();
+
+ /**
+ * Write the given object to the database.
+ *
+ * This object is an instance of a class
+ * annotated with {@link Collection}, and
+ * contains fields annotated with {@link Field}.
+ *
+ *
+ * @param element the element to write
+ */
+ void write(@NonNull Object element);
}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/feather/annotation/Collection.java b/src/main/java/me/braydon/feather/annotation/Collection.java
new file mode 100644
index 0000000..c11c634
--- /dev/null
+++ b/src/main/java/me/braydon/feather/annotation/Collection.java
@@ -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 "";
+}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/feather/annotation/Field.java b/src/main/java/me/braydon/feather/annotation/Field.java
new file mode 100644
index 0000000..5477af6
--- /dev/null
+++ b/src/main/java/me/braydon/feather/annotation/Field.java
@@ -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.
+ *
+ * If empty, the field name
+ * will be used as the key.
+ *
+ *
+ * @return the key
+ */
+ String key() default "";
+}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/feather/annotation/Id.java b/src/main/java/me/braydon/feather/annotation/Id.java
new file mode 100644
index 0000000..17cbd33
--- /dev/null
+++ b/src/main/java/me/braydon/feather/annotation/Id.java
@@ -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";
+}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/feather/data/Document.java b/src/main/java/me/braydon/feather/data/Document.java
new file mode 100644
index 0000000..c377fb0
--- /dev/null
+++ b/src/main/java/me/braydon/feather/data/Document.java
@@ -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 the type of value this document holds
+ */
+@Getter @ToString
+public class Document {
+ /**
+ * 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 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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/feather/databases/mongodb/MongoDB.java b/src/main/java/me/braydon/feather/databases/mongodb/MongoDB.java
index 53cd4b8..cbb4c17 100644
--- a/src/main/java/me/braydon/feather/databases/mongodb/MongoDB.java
+++ b/src/main/java/me/braydon/feather/databases/mongodb/MongoDB.java
@@ -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 MongoDB Official Site
+ * @see MongoDB Official Site
*/
public class MongoDB implements IDatabase {
/**
@@ -109,6 +115,38 @@ public class MongoDB implements IDatabase
+ * This object is an instance of a class
+ * annotated with {@link Collection}, and
+ * contains fields annotated with {@link Field}.
+ *
+ *
+ * @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 collection = database.getCollection(collectionName); // Get the collection
+ Document