Changed a lot, now allows loading of entities

This commit is contained in:
Braydon 2023-12-15 01:46:16 -05:00
parent 8cb196233a
commit 4828933702
16 changed files with 256 additions and 231 deletions

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather;
import com.google.gson.Gson;

@ -1,24 +0,0 @@
package me.braydon.feather.annotation;
import me.braydon.feather.data.Document;
import java.lang.annotation.*;
/**
* Classes tagged with this annotation
* will be treated as a collection that
* holds {@link Document}'s.
*
* @author Braydon
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented @Inherited
public @interface Collection {
/**
* The name of this collection.
*
* @return the name
*/
String name() default "";
}

@ -1,10 +1,15 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.annotation;
import java.lang.annotation.*;
/**
* Fields tagged with this annotation will be
* treated as a field within a {@link Collection}.
* treated as a field within a collection.
*
* @author Braydon
*/

@ -1,11 +1,18 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.annotation;
import me.braydon.feather.data.Document;
import java.lang.annotation.*;
/**
* {@link Field}'s tagged with this annotation will be
* treated as the primary identifying key for documents
* within a {@link Collection}.
* {@link Field}'s tagged with this annotation
* will be treated as the primary identifying
* key for {@link Document}'s within a collection.
*
* @author Braydon
*/

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.annotation;
import com.google.gson.Gson;

@ -1,29 +0,0 @@
package me.braydon.feather.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import me.braydon.feather.annotation.Collection;
/**
* @author Braydon
*/
@UtilityClass
public final class EntityUtils {
/**
* Ensure that the given entity is valid.
*
* @param entity the entity to validate
* @param allowEmptyCollections should empty collections be allowed?
*/
public static void ensureValid(@NonNull Object entity, boolean allowEmptyCollections) {
Class<?> clazz = entity.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() && !allowEmptyCollections) { // Missing collection name
throw new IllegalStateException("Missing collection name in @Collection for " + clazz.getSimpleName());
}
}
}

@ -0,0 +1,35 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import me.braydon.feather.annotation.Id;
import java.lang.reflect.Field;
/**
* @author Braydon
*/
@UtilityClass
public final class FieldUtils {
/**
* Extract the key to use for the given field.
*
* @param field the field to get the key from
* @return the key for the field
*/
@NonNull
public static String extractKey(@NonNull Field field) {
boolean idField = field.isAnnotationPresent(Id.class); // Is this field annotated with @Id?
me.braydon.feather.annotation.Field annotation = field.getAnnotation(me.braydon.feather.annotation.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();
}
return key;
}
}

@ -1,9 +1,11 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
/**
* Represents an object that
@ -13,7 +15,7 @@ import lombok.Setter;
* @param <L> the left value
* @param <R> the right value
*/
@NoArgsConstructor @AllArgsConstructor @Setter @Getter
@NoArgsConstructor @AllArgsConstructor @Setter @Getter @ToString
public class Tuple<L, R> {
/**
* The left value of this tuple.

@ -1,13 +1,18 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.data;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import me.braydon.feather.FeatherSettings;
import me.braydon.feather.annotation.Collection;
import me.braydon.feather.annotation.Field;
import me.braydon.feather.annotation.Id;
import me.braydon.feather.annotation.Serializable;
import me.braydon.feather.common.FieldUtils;
import me.braydon.feather.common.Tuple;
import me.braydon.feather.database.IDatabase;
@ -18,11 +23,10 @@ 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.
* A document is a key-value pair that is stored within
* a 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
@ -61,14 +65,10 @@ public class Document<V> {
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();
}
String key = FieldUtils.extractKey(field); // The key of the database field
// The field is annotated with @Id, save it for later
if (idField) {
if (field.isAnnotationPresent(Id.class)) {
idKey = key;
}
Class<?> fieldType = field.getType(); // The type of the field
@ -87,7 +87,7 @@ public class Document<V> {
}
}
assert idKey != null; // We need an id key
this.idKey = idKey; // We have our id key
this.idKey = idKey; // Set our id key
Tuple<java.lang.reflect.Field, V> key = mappedData.get(idKey); // Get the id from the data map
if (key == null) { // The element is missing an id field

@ -1,7 +1,11 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database;
import lombok.NonNull;
import me.braydon.feather.repository.Repository;
import java.io.Closeable;
@ -11,9 +15,8 @@ import java.io.Closeable;
* @author Braydon
* @param <B> the bootstrap class of this database
* @param <C> the type of credentials this database uses
* @param <R> the type of repository for this database
*/
public interface IDatabase<B, C, R extends Repository<?, ?, ?>> extends Closeable {
public interface IDatabase<B, C> extends Closeable {
/**
* Get the name of this database.
*
@ -25,8 +28,9 @@ public interface IDatabase<B, C, R extends Repository<?, ?, ?>> extends Closeabl
* Initialize a connection to this database.
*
* @param credentials the optional credentials to use
* @throws IllegalStateException if already connected
*/
void connect(C credentials);
void connect(C credentials) throws IllegalStateException;
/**
* Check if this database is connected.
@ -50,15 +54,4 @@ public interface IDatabase<B, C, R extends Repository<?, ?, ?>> extends Closeabl
* @see B for bootstrap class
*/
B getBootstrap();
/**
* Create a new repository
* using this database.
*
* @param <ID> the id type
* @param <E> the entity type
* @return the repository instance
* @see R for repository
*/
@NonNull <ID, E> R newRepository();
}

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.mongodb;
import com.mongodb.BasicDBObject;
@ -15,10 +20,9 @@ import me.braydon.feather.database.IDatabase;
* @author Braydon
* @see MongoClient for the bootstrap class
* @see ConnectionString for the credentials class
* @see MongoRepository for the repository class
* @see <a href="https://www.mongodb.com">MongoDB Official Site</a>
*/
public class MongoDB implements IDatabase<MongoClient, ConnectionString, MongoRepository<?, ?>> {
public class MongoDB implements IDatabase<MongoClient, ConnectionString> {
/**
* The current {@link MongoClient} instance.
*/
@ -43,9 +47,11 @@ public class MongoDB implements IDatabase<MongoClient, ConnectionString, MongoRe
* Initialize a connection to this database.
*
* @param credentials the optional credentials to use
* @throws IllegalArgumentException if no credentials or database name is provided
* @throws IllegalStateException if already connected
*/
@Override
public void connect(ConnectionString credentials) {
public void connect(ConnectionString credentials) throws IllegalArgumentException, IllegalStateException {
if (credentials == null) { // We need valid credentials
throw new IllegalArgumentException("No credentials defined");
}
@ -102,15 +108,22 @@ public class MongoDB implements IDatabase<MongoClient, ConnectionString, MongoRe
}
/**
* Create a new repository
* using this database.
* Create a new repository using this database.
*
* @param <ID> the identifier for type for entities
* @param <E> the entity type the repository stores
* @param collectionName the collection name for the repository
* @param entityClass the class of the entity the repository uses
* @return the repository instance
* @throws IllegalStateException if not connected
* @see MongoRepository for repository
*/
@Override @NonNull
public <ID, E> MongoRepository<ID, E> newRepository() {
return new MongoRepository<>(this);
@NonNull
public <ID, E> MongoRepository<ID, E> newRepository(@NonNull String collectionName, @NonNull Class<? extends E> entityClass) {
if (!isConnected()) { // Not connected
throw new IllegalStateException("Not connected");
}
return new MongoRepository<>(this, entityClass, database.getCollection(collectionName));
}
/**

@ -1,13 +1,17 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.mongodb;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Indexes;
import com.mongodb.client.model.UpdateOneModel;
import com.mongodb.client.model.UpdateOptions;
import lombok.NonNull;
import me.braydon.feather.annotation.Collection;
import me.braydon.feather.common.EntityUtils;
import me.braydon.feather.common.Tuple;
import me.braydon.feather.database.impl.mongodb.annotation.Index;
import me.braydon.feather.repository.Repository;
@ -15,10 +19,9 @@ import org.bson.Document;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
/**
* The {@link MongoDB} {@link Repository} implementation.
@ -28,8 +31,14 @@ import java.util.function.Predicate;
* @param <E> the entity type this repository stores
*/
public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
public MongoRepository(@NonNull MongoDB database) {
super(database);
/**
* The {@link MongoCollection} to use for this repository.
*/
@NonNull private final MongoCollection<Document> collection;
public MongoRepository(@NonNull MongoDB database, @NonNull Class<? extends E> entityClass, @NonNull MongoCollection<Document> collection) {
super(database, entityClass);
this.collection = collection;
}
/**
@ -42,34 +51,20 @@ public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
*/
@Override
public E find(@NonNull ID id) {
String idString = id.toString(); // Stringify the ID
throw new UnsupportedOperationException();
return find("_id", id);
}
/**
* Find the entity matching the given predicate.
* Get the entity with the given id.
*
* @param predicate the predicate to test
* @return the found entity
* @param idKey the key of the id
* @param id the entity id
* @return the entity with the id, null if none
* @see ID for id
* @see E for entity
* @see Predicate for predicate
*/
@Override
public E findOne(@NonNull Predicate<E> predicate) {
throw new UnsupportedOperationException();
}
/**
* Find all entities matching the given predicate.
*
* @param predicate the predicate to test
* @return the found entities
* @see E for entity
* @see Predicate for predicate
*/
@Override
public List<E> findAll(@NonNull Predicate<E> predicate) {
throw new UnsupportedOperationException();
public E find(@NonNull String idKey, @NonNull ID id) {
return newEntity(collection.find(new Document(idKey, id.toString())).first());
}
/**
@ -80,7 +75,13 @@ public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
*/
@Override
public List<E> findAll() {
throw new UnsupportedOperationException();
List<E> entities = new ArrayList<>();
try (MongoCursor<Document> cursor = collection.find().cursor()) {
while (cursor.hasNext()) { // Add the entity to the list
entities.add(newEntity(cursor.next()));
}
}
return Collections.unmodifiableList(entities);
}
/**
@ -91,36 +92,19 @@ public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
*/
@Override
public void saveAll(@NonNull E... entities) {
Map<String, List<me.braydon.feather.data.Document<Object>>> toSave = new HashMap<>(); // The documents to save
List<UpdateOneModel<Document>> updateModels = new ArrayList<>(); // The update models to bulk write
// Iterate over the given entities and ensure they
// are all valid, and if they are, collect them so
// we can bulk save them later
for (E entity : entities) {
EntityUtils.ensureValid(entity, false); // Ensure our entity is valid
String collectionName = entity.getClass().getAnnotation(Collection.class).name(); // The name of the collection
me.braydon.feather.data.Document<Object> document = new me.braydon.feather.data.Document<>(entity); // Create a document from the entity
// Add the document to our list of documents to save
List<me.braydon.feather.data.Document<Object>> documents = toSave.getOrDefault(collectionName, new ArrayList<>());
documents.add(new me.braydon.feather.data.Document<>(entity));
toSave.put(collectionName, documents);
}
// Iterate over the documents we want to save, and create
// an update model for them, as well as update indexes.
for (Map.Entry<String, List<me.braydon.feather.data.Document<Object>>> entry : toSave.entrySet()) {
MongoCollection<Document> collection = getDatabase().getDatabase().getCollection(entry.getKey()); // The collection to save to
List<UpdateOneModel<Document>> updateModels = new ArrayList<>();
for (me.braydon.feather.data.Document<Object> document : entry.getValue()) {
// Add or update model to the list
// Add our update model to the list
updateModels.add(new UpdateOneModel<>(
Filters.eq(document.getIdKey(), document.getKey()),
new Document("$set", new Document(document.toMappedData())),
new UpdateOptions().upsert(true)
));
// Create indexes for @Index fields
// Create indexes for @Index fields specified in the entity
for (Map.Entry<String, Tuple<Field, Object>> mappedEntry : document.getMappedData().entrySet()) {
java.lang.reflect.Field field = mappedEntry.getValue().getLeft();
if (field.isAnnotationPresent(Index.class)) {
@ -129,12 +113,11 @@ public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
}
}
// We have updates models present, bulk write them to the database
// We have update models to execute, bulk write them
if (!updateModels.isEmpty()) {
collection.bulkWrite(updateModels);
}
}
}
/**
* Get the amount of stored entities.
@ -144,7 +127,7 @@ public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
*/
@Override
public long count() {
throw new UnsupportedOperationException();
return collection.countDocuments();
}
/**
@ -155,6 +138,7 @@ public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
*/
@Override
public void drop(@NonNull E entity) {
throw new UnsupportedOperationException();
me.braydon.feather.data.Document<Object> document = new me.braydon.feather.data.Document<>(entity); // Create a document from the entity
collection.deleteOne(new Document(document.getIdKey(), document.getKey())); // Delete the entity
}
}

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.mongodb.annotation;
import me.braydon.feather.database.impl.mongodb.MongoDB;

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.redis;
import io.lettuce.core.RedisClient;
@ -12,10 +17,9 @@ import me.braydon.feather.database.IDatabase;
* @author Braydon
* @see StatefulRedisConnection for the bootstrap class
* @see RedisURI for the credentials class
* @see RedisRepository for the repository class
* @see <a href="https://redis.io">Redis Official Site</a>
*/
public class Redis implements IDatabase<StatefulRedisConnection<String, String>, RedisURI, RedisRepository<?, ?>> {
public class Redis implements IDatabase<StatefulRedisConnection<String, String>, RedisURI> {
/**
* The current {@link RedisClient} instance.
*/
@ -40,9 +44,11 @@ public class Redis implements IDatabase<StatefulRedisConnection<String, String>,
* Initialize a connection to this database.
*
* @param credentials the optional credentials to use
* @throws IllegalArgumentException if no credentials are provided
* @throws IllegalStateException if already connected
*/
@Override
public void connect(RedisURI credentials) {
public void connect(RedisURI credentials) throws IllegalArgumentException, IllegalStateException {
if (credentials == null) { // We need valid credentials
throw new IllegalArgumentException("No credentials defined");
}
@ -91,17 +97,17 @@ public class Redis implements IDatabase<StatefulRedisConnection<String, String>,
return connection;
}
/**
* Create a new repository
* using this database.
*
* @return the repository instance
* @see RedisRepository for repository
*/
@Override @NonNull
public <ID, E> RedisRepository<ID, E> newRepository() {
return new RedisRepository<>(this);
}
// /**
// * Create a new repository
// * using this database.
// *
// * @return the repository instance
// * @see RedisRepository for repository
// */
// @NonNull
// public <ID, E> RedisRepository<ID, E> newRepository() {
// return new RedisRepository<>(this);
// }
// @Override
// public void write(@NonNull Object element) {

@ -1,10 +1,14 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.redis;
import lombok.NonNull;
import me.braydon.feather.repository.Repository;
import java.util.List;
import java.util.function.Predicate;
/**
* The {@link Redis} {@link Repository} implementation.
@ -14,8 +18,8 @@ import java.util.function.Predicate;
* @param <E> the entity type this repository stores
*/
public class RedisRepository<ID, E> extends Repository<Redis, ID, E> {
public RedisRepository(@NonNull Redis database) {
super(database);
public RedisRepository(@NonNull Redis database, @NonNull Class<? extends E> entityClass) {
super(database, entityClass);
}
/**
@ -31,32 +35,6 @@ public class RedisRepository<ID, E> extends Repository<Redis, ID, E> {
throw new UnsupportedOperationException();
}
/**
* Find the entity matching the given predicate.
*
* @param predicate the predicate to test
* @return the found entity
* @see E for entity
* @see Predicate for predicate
*/
@Override
public E findOne(@NonNull Predicate<E> predicate) {
throw new UnsupportedOperationException();
}
/**
* Find all entities matching the given predicate.
*
* @param predicate the predicate to test
* @return the found entities
* @see E for entity
* @see Predicate for predicate
*/
@Override
public List<E> findAll(@NonNull Predicate<E> predicate) {
throw new UnsupportedOperationException();
}
/**
* Get all entities within this repository.
*

@ -1,13 +1,25 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.repository;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import me.braydon.feather.FeatherSettings;
import me.braydon.feather.annotation.Serializable;
import me.braydon.feather.common.FieldUtils;
import me.braydon.feather.database.IDatabase;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.function.Predicate;
import java.util.Map;
import java.util.UUID;
/**
* A repository belonging to a {@link IDatabase}.
@ -18,7 +30,7 @@ import java.util.function.Predicate;
* @param <E> the entity type this repository stores
*/
@AllArgsConstructor @Getter(AccessLevel.PROTECTED)
public abstract class Repository<D extends IDatabase<?, ?, ?>, ID, E> {
public abstract class Repository<D extends IDatabase<?, ?>, ID, E> {
/**
* The database this repository belongs to.
*
@ -26,6 +38,13 @@ public abstract class Repository<D extends IDatabase<?, ?, ?>, ID, E> {
*/
@NonNull private final D database;
/**
* The class for the entity this repository uses.
*
* @see E for entity
*/
@NonNull private final Class<? extends E> entityClass;
/**
* Get the entity with the given id.
*
@ -36,26 +55,6 @@ public abstract class Repository<D extends IDatabase<?, ?, ?>, ID, E> {
*/
public abstract E find(@NonNull ID id);
/**
* Find the entity matching the given predicate.
*
* @param predicate the predicate to test
* @return the found entity
* @see E for entity
* @see Predicate for predicate
*/
public abstract E findOne(@NonNull Predicate<E> predicate);
/**
* Find all entities matching the given predicate.
*
* @param predicate the predicate to test
* @return the found entities
* @see E for entity
* @see Predicate for predicate
*/
public abstract List<E> findAll(@NonNull Predicate<E> predicate);
/**
* Get all entities within this repository.
*
@ -97,4 +96,45 @@ public abstract class Repository<D extends IDatabase<?, ?, ?>, ID, E> {
* @see E for entity
*/
public abstract void drop(@NonNull E entity);
/**
* Construct a new entity from the given mapped data.
*
* @param mappedData the mapped data to parse
* @return the created entity, null if none
* @see E for entity
*/
protected final E newEntity(Map<String, Object> mappedData) {
if (mappedData == null) { // No mapped data given
return null;
}
try {
Constructor<? extends E> constructor = entityClass.getConstructor(); // Get the no args constructor
E entity = constructor.newInstance(); // Create the entity
// Get the field tagged with @Id
for (Field field : entityClass.getDeclaredFields()) {
String key = FieldUtils.extractKey(field); // The key of the database field
Class<?> type = field.getType(); // The type of the field
Object value = mappedData.get(key); // The value of the field
// Field is serializable and is a string, deserialize it using Gson
if (field.isAnnotationPresent(Serializable.class) && value.getClass() == String.class) {
value = FeatherSettings.getGson().fromJson((String) value, type);
} else if (type == UUID.class) { // Type is a UUID, convert it
value = UUID.fromString((String) value);
}
// Set the value of the field
field.setAccessible(true);
field.set(entity, value);
}
return entity;
} catch (NoSuchMethodException ex) { // We need our no args constructor
throw new IllegalStateException("Entity " + entityClass.getName() + " is missing no args constructor");
} catch (InvocationTargetException | InstantiationException | IllegalAccessException ex) {
ex.printStackTrace();
}
return null;
}
}