Compare commits

..

26 Commits

Author SHA1 Message Date
716e2934cc oops 2023-12-17 21:27:04 -05:00
2626ba7ffd Fix checkstyle violation 2023-12-17 21:22:20 -05:00
b28d9e0645 Remove generics from Document 2023-12-17 21:20:43 -05:00
edbb75d292 Update .gitignore 2023-12-17 21:07:45 -05:00
6b215cc337 Catch errs for Redis 2023-12-17 21:07:22 -05:00
2ec91e3dcf Allow for a raw Mongo collection to be passed to Mongo repos 2023-12-17 02:16:02 -05:00
e8f3b45c68 Bump Lettuce ver 2023-12-16 20:20:02 -05:00
3567912fd9 Shade Netty in the final jar 2023-12-16 19:51:01 -05:00
32e8e2deb8 Fix Netty err with Redis? 2023-12-16 19:41:30 -05:00
f44e19f6d5 Add @CustomDocument annotation 2023-12-16 19:11:24 -05:00
bd8e3b284b Use @RawData for fetching too, oops 2023-12-16 18:36:59 -05:00
a5d7837c7d oops, forgot to check for @Field 2023-12-16 18:27:27 -05:00
06a716dc00 Fix NPE? 2023-12-16 18:20:28 -05:00
7a168bbb05 Allow a Mongo repo to have a collection name specified 2023-12-16 18:13:06 -05:00
71af157e3a oops, use this map 2023-12-16 18:00:14 -05:00
fd45d8af02 work? 2023-12-16 17:58:31 -05:00
f2d5f203c6 goodbye big jar 2023-12-16 17:49:50 -05:00
0c2465df58 Fix Javadoc err 2023-12-16 17:49:42 -05:00
caebbc59a0 Update workflow and make the checkstyle less strict 2023-12-16 17:47:22 -05:00
3119173d87 Properly shutdown Redis 2023-12-16 15:51:33 -05:00
0742ab40d7 Begin work on MariaDB 2023-12-15 05:35:47 -05:00
edc88bdb9a feat: Add @TTL to allow expiration of keys 2023-12-15 04:24:53 -05:00
0f75a1c261 ci: Remove deploy script 2023-12-15 04:15:32 -05:00
1b17ed8d79 These don't need to compile with the jar 2023-12-15 02:57:51 -05:00
98016830dd Remove Guava depend 2023-12-15 02:57:38 -05:00
ba15dfc9d4 Bump version to 1.0.0-dev 2023-12-15 02:55:47 -05:00
19 changed files with 548 additions and 85 deletions

@ -2,7 +2,7 @@ name: Publish Release
on: on:
push: push:
branches: [master] branches: [master, develop]
tags: tags:
- '*' - '*'
@ -58,14 +58,13 @@ jobs:
# Publish to Maven # Publish to Maven
- name: Publish to Maven - name: Publish to Maven
run: | run: mvn deploy -Pgen-javadocs -B -Dstyle.color=always --update-snapshots -T6C -e
chmod +x ./scripts/deploy.sh
./scripts/deploy.sh
# Generate changelog # Generate changelog
- name: Generate Changelog - name: Generate Changelog
id: changelog id: changelog
uses: https://github.com/mathiasvr/command-output@v2.0.0 uses: https://github.com/mathiasvr/command-output@v2.0.0
if: ${{ github.ref.action == 'master' }}
with: with:
run: | run: |
npx conventional-changelog-cli -p angular -i CHANGELOG.md -s --skip-unstable npx conventional-changelog-cli -p angular -i CHANGELOG.md -s --skip-unstable
@ -76,6 +75,7 @@ jobs:
# so we can use it later # so we can use it later
- name: Extract Maven project version - name: Extract Maven project version
shell: bash -l {0} shell: bash -l {0}
if: ${{ github.ref.action == 'master' }}
run: | run: |
RELEASE_VERSION=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) RELEASE_VERSION=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)
echo "Project version: $RELEASE_VERSION" echo "Project version: $RELEASE_VERSION"
@ -85,6 +85,7 @@ jobs:
- name: Publish Release - name: Publish Release
id: publish-release id: publish-release
uses: https://git.rainnny.club/Rainnny/release-action@main uses: https://git.rainnny.club/Rainnny/release-action@main
if: ${{ github.ref.action == 'master' }}
with: with:
api_key: ${{ secrets.RELEASE_ACCESS_TOKEN }} api_key: ${{ secrets.RELEASE_ACCESS_TOKEN }}
tag: Feather-${{ env.RELEASE_VERSION }} tag: Feather-${{ env.RELEASE_VERSION }}

1
.gitignore vendored

@ -18,6 +18,7 @@ cmake-build-*/
out/ out/
build/ build/
work/ work/
target/
.idea_modules/ .idea_modules/
atlassian-ide-plugin.xml atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml com_crashlytics_export_strings.xml

@ -91,9 +91,6 @@
<!-- See: https://checkstyle.org/checks/javadoc/javadocmissingwhitespaceafterasterisk.html#JavadocMissingWhitespaceAfterAsterisk --> <!-- See: https://checkstyle.org/checks/javadoc/javadocmissingwhitespaceafterasterisk.html#JavadocMissingWhitespaceAfterAsterisk -->
<module name="JavadocMissingWhitespaceAfterAsterisk" /> <module name="JavadocMissingWhitespaceAfterAsterisk" />
<!-- See: https://checkstyle.org/checks/javadoc/javadocparagraph.html#JavadocParagraph -->
<module name="JavadocParagraph" />
<!-- See: https://checkstyle.org/checks/javadoc/javadocstyle.html#JavadocStyle --> <!-- See: https://checkstyle.org/checks/javadoc/javadocstyle.html#JavadocStyle -->
<module name="JavadocStyle" /> <module name="JavadocStyle" />
@ -215,11 +212,6 @@
<!-- See: https://checkstyle.org/checks/javadoc/singlelinejavadoc.html#SingleLineJavadoc --> <!-- See: https://checkstyle.org/checks/javadoc/singlelinejavadoc.html#SingleLineJavadoc -->
<module name="SingleLineJavadoc" /> <module name="SingleLineJavadoc" />
<!-- See: https://checkstyle.org/checks/javadoc/requireemptylinebeforeblocktaggroup.html#RequireEmptyLineBeforeBlockTagGroup -->
<module name="RequireEmptyLineBeforeBlockTagGroup">
<property name="violateExecutionOnNonTightHtml" value="true" />
</module>
<!-- See: https://checkstyle.org/checks/whitespace/singlespaceseparator.html#SingleSpaceSeparator --> <!-- See: https://checkstyle.org/checks/whitespace/singlespaceseparator.html#SingleSpaceSeparator -->
<module name="SingleSpaceSeparator" /> <module name="SingleSpaceSeparator" />

51
pom.xml

@ -7,7 +7,7 @@
<!--Project Details--> <!--Project Details-->
<groupId>me.braydon</groupId> <groupId>me.braydon</groupId>
<artifactId>Feather</artifactId> <artifactId>Feather</artifactId>
<version>1.0.0</version> <version>1.0.1-dev</version>
<properties> <properties>
<java.version>8</java.version> <java.version>8</java.version>
@ -18,7 +18,7 @@
<build> <build>
<plugins> <plugins>
<!--Used for compiling the source code with the proper Java version--> <!-- Used for compiling the source code with the proper Java version -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
@ -27,17 +27,17 @@
<source>${java.version}</source> <source>${java.version}</source>
<target>${java.version}</target> <target>${java.version}</target>
<!--Enable incremental builds, this is reversed due to--> <!-- Enable incremental builds, this is reversed due to -->
<!--a bug as seen in https://issues.apache.org/jira/browse/MCOMPILER-209--> <!-- a bug as seen in https://issues.apache.org/jira/browse/MCOMPILER-209 -->
<useIncrementalCompilation>false</useIncrementalCompilation> <useIncrementalCompilation>false</useIncrementalCompilation>
</configuration> </configuration>
</plugin> </plugin>
<!--Handles shading of dependencies in the final output jar--> <!-- Handles shading of dependencies in the final output jar -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version> <version>3.5.1</version>
<configuration> <configuration>
<createDependencyReducedPom>false</createDependencyReducedPom> <createDependencyReducedPom>false</createDependencyReducedPom>
</configuration> </configuration>
@ -70,7 +70,7 @@
</executions> </executions>
</plugin> </plugin>
<!--Used for generating a git properties file during build--> <!-- Used for generating a git properties file during build -->
<plugin> <plugin>
<groupId>pl.project13.maven</groupId> <groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId> <artifactId>git-commit-id-plugin</artifactId>
@ -168,6 +168,13 @@
</repository> </repository>
</distributionManagement> </distributionManagement>
<repositories>
<repository>
<id>rainnny-repo-public</id>
<url>https://maven.rainnny.club/public</url>
</repository>
</repositories>
<!-- Depends --> <!-- Depends -->
<dependencies> <dependencies>
<dependency> <dependency>
@ -176,31 +183,43 @@
<version>1.18.30</version> <version>1.18.30</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>com.google.code.gson</groupId> <groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId> <artifactId>gson</artifactId>
<version>2.10.1</version> <version>2.10.1</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
<version>4.1.104.Final</version>
<scope>compile</scope>
</dependency>
<!-- Databases --> <!-- Databases -->
<dependency> <dependency>
<groupId>org.mongodb</groupId> <groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId> <artifactId>mongodb-driver-sync</artifactId>
<version>4.11.1</version> <version>4.11.1</version>
<scope>compile</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.lettuce</groupId> <groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId> <artifactId>lettuce-core</artifactId>
<version>6.3.2.RELEASE</version> <version>7.0.0-SNAPSHOT</version>
<scope>compile</scope> <scope>provided</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.3.1</version>
<scope>provided</scope>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

@ -1,7 +0,0 @@
#!/bin/bash
# Switch dir
cd "$(dirname "$0")/.." || exit
# Deploy
mvn deploy -Pgen-javadocs -B -Dstyle.color=always --update-snapshots -T6C -e

@ -0,0 +1,20 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.annotation;
import java.lang.annotation.*;
/**
* {@link Field}'s tagged with this annotation
* will be set to the raw data from the document
* it is in.
*
* @author Braydon
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented @Inherited
public @interface RawData { }

@ -7,16 +7,17 @@ package me.braydon.feather.data;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.ToString; import lombok.ToString;
import me.braydon.feather.FeatherSettings; import me.braydon.feather.FeatherSettings;
import me.braydon.feather.annotation.Field; import me.braydon.feather.annotation.Field;
import me.braydon.feather.annotation.Id; import me.braydon.feather.annotation.Id;
import me.braydon.feather.annotation.RawData;
import me.braydon.feather.annotation.Serializable; import me.braydon.feather.annotation.Serializable;
import me.braydon.feather.common.FieldUtils; import me.braydon.feather.common.FieldUtils;
import me.braydon.feather.common.Tuple; import me.braydon.feather.common.Tuple;
import me.braydon.feather.database.IDatabase; import me.braydon.feather.database.IDatabase;
import javax.annotation.concurrent.ThreadSafe;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@ -29,10 +30,9 @@ import java.util.UUID;
* document is universal between all {@link IDatabase}'s. * document is universal between all {@link IDatabase}'s.
* *
* @author Braydon * @author Braydon
* @param <V> the type of value this document holds
*/ */
@ThreadSafe @Getter @ToString @Getter @ToString
public class Document<V> { public class Document {
/** /**
* The key to use for the id field. * The key to use for the id field.
*/ */
@ -51,15 +51,20 @@ public class Document<V> {
* a tuple that contains the Java field, as well as * a tuple that contains the Java field, as well as
* the field value. * the field value.
* </p> * </p>
*
* @see V for value type
*/ */
private final Map<String, Tuple<java.lang.reflect.Field, V>> mappedData = Collections.synchronizedMap(new LinkedHashMap<>()); private final Map<String, Tuple<java.lang.reflect.Field, Object>> mappedData = Collections.synchronizedMap(new LinkedHashMap<>());
@SneakyThrows
public Document(@NonNull Object element) { public Document(@NonNull Object element) {
Class<?> clazz = element.getClass(); // Get the element class Class<?> clazz = element.getClass(); // Get the element class
String idKey = null; // The key for the id field String idKey = null; // The key for the id field
java.lang.reflect.Field rawDataField = null; // The raw data field if defined
for (java.lang.reflect.Field field : clazz.getDeclaredFields()) { for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
// Raw data field, save it for later
if (field.isAnnotationPresent(RawData.class)) {
rawDataField = field;
continue;
}
// Field is missing the @Field annotation, skip it // Field is missing the @Field annotation, skip it
if (!field.isAnnotationPresent(Field.class)) { if (!field.isAnnotationPresent(Field.class)) {
continue; continue;
@ -72,24 +77,24 @@ public class Document<V> {
idKey = key; idKey = key;
} }
Class<?> fieldType = field.getType(); // The type of the field Class<?> fieldType = field.getType(); // The type of the field
try { Object value = field.get(element); // The value of the field
Object value = field.get(element); // The value of the field
if (field.isAnnotationPresent(Serializable.class)) { // Serialize the field if @Serializable is present
if (field.isAnnotationPresent(Serializable.class)) { // Serialize the field if @Serializable is present value = FeatherSettings.getGson().toJson(field.get(element));
value = FeatherSettings.getGson().toJson(field.get(element)); } else if (fieldType == UUID.class) { // Convert UUIDs into strings
} else if (fieldType == UUID.class) { // Convert UUIDs into strings value = value.toString();
value = value.toString();
}
mappedData.put(key, new Tuple<>(field, (V) value)); // Store in our map
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
} }
mappedData.put(key, new Tuple<>(field, value)); // Store in our map
} }
assert idKey != null; // We need an id key assert idKey != null; // We need an id key
if (rawDataField != null) { // We have a raw data field, set it
rawDataField.setAccessible(true); // Make our field accessible
rawDataField.set(element, toMappedData()); // Set the raw data field to our mapped document
}
this.idKey = idKey; // Set 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 Tuple<java.lang.reflect.Field, Object> key = mappedData.get(idKey); // Get the id from the data map
if (key == null) { // The element is missing an id field if (key == null) { // The element is missing an id field
throw new IllegalArgumentException("No @Id annotated field found in " + clazz.getSimpleName()); throw new IllegalArgumentException("No @Id annotated field found in " + clazz.getSimpleName());
} }
@ -103,9 +108,9 @@ public class Document<V> {
* @see #mappedData for stored data * @see #mappedData for stored data
*/ */
@NonNull @NonNull
public Map<String, V> toMappedData() { public Map<String, Object> toMappedData() {
Map<String, V> mappedData = new LinkedHashMap<>(); // The mapped data Map<String, Object> mappedData = new LinkedHashMap<>(); // The mapped data
for (Map.Entry<String, Tuple<java.lang.reflect.Field, V>> entry : this.mappedData.entrySet()) { for (Map.Entry<String, Tuple<java.lang.reflect.Field, Object>> entry : this.mappedData.entrySet()) {
mappedData.put(entry.getKey(), entry.getValue().getRight()); mappedData.put(entry.getKey(), entry.getValue().getRight());
} }
return mappedData; return mappedData;

@ -29,6 +29,7 @@ public interface IDatabase<B, C> extends Closeable {
* *
* @param credentials the optional credentials to use * @param credentials the optional credentials to use
* @throws IllegalStateException if already connected * @throws IllegalStateException if already connected
* @see C for credentials
*/ */
void connect(C credentials) throws IllegalStateException; void connect(C credentials) throws IllegalStateException;

@ -10,6 +10,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import me.braydon.feather.FeatherSettings; import me.braydon.feather.FeatherSettings;
import me.braydon.feather.annotation.RawData;
import me.braydon.feather.annotation.Serializable; import me.braydon.feather.annotation.Serializable;
import me.braydon.feather.common.FieldUtils; import me.braydon.feather.common.FieldUtils;
@ -21,7 +22,7 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
* A repository belonging to a {@link IDatabase}. * A repository belonging to an {@link IDatabase}.
* *
* @author Braydon * @author Braydon
* @param <D> the database this repository uses * @param <D> the database this repository uses
@ -120,8 +121,18 @@ public abstract class Repository<D extends IDatabase<?, ?>, ID, E> {
Constructor<? extends E> constructor = entityClass.getConstructor(); // Get the no args constructor Constructor<? extends E> constructor = entityClass.getConstructor(); // Get the no args constructor
E entity = constructor.newInstance(); // Create the entity E entity = constructor.newInstance(); // Create the entity
// Get the field tagged with @Id // Get the field tagged with @Field
java.lang.reflect.Field rawDataField = null; // The raw data field if defined
for (Field field : entityClass.getDeclaredFields()) { for (Field field : entityClass.getDeclaredFields()) {
// Raw data field, save it for later
if (field.isAnnotationPresent(RawData.class)) {
rawDataField = field;
continue;
}
// Not the field we're looking for
if (!field.isAnnotationPresent(me.braydon.feather.annotation.Field.class)) {
continue;
}
String key = FieldUtils.extractKey(field); // The key of the database field String key = FieldUtils.extractKey(field); // The key of the database field
Class<?> type = field.getType(); // The type of the field Class<?> type = field.getType(); // The type of the field
Object value = mappedData.get(key); // The value of the field Object value = mappedData.get(key); // The value of the field
@ -137,6 +148,10 @@ public abstract class Repository<D extends IDatabase<?, ?>, ID, E> {
field.setAccessible(true); field.setAccessible(true);
field.set(entity, value); field.set(entity, value);
} }
if (rawDataField != null) { // We have a raw data field, set it
rawDataField.setAccessible(true); // Make our field accessible
rawDataField.set(entity, mappedData); // Set the raw data field to our mapped document
}
return entity; return entity;
} catch (NoSuchMethodException ex) { // We need our no args constructor } catch (NoSuchMethodException ex) { // We need our no args constructor
throw new IllegalStateException("Entity " + entityClass.getName() + " is missing no args constructor"); throw new IllegalStateException("Entity " + entityClass.getName() + " is missing no args constructor");

@ -0,0 +1,110 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.mariadb;
import com.zaxxer.hikari.HikariDataSource;
import lombok.NonNull;
import me.braydon.feather.database.IDatabase;
/**
* The {@link IDatabase} implementation for MariaDB (and other MySQL servers).
*
* @author Braydon
* @see HikariDataSource for the bootstrap class
* @see MariaDBAuthorization for the credentials class
* @see <a href="https://github.com/brettwooldridge/HikariCP">HikariCP Official GitHub</a>
*/
public class MariaDB implements IDatabase<HikariDataSource, MariaDBAuthorization> {
/**
* The current {@link HikariDataSource} instance.
*/
private HikariDataSource dataSource;
/**
* Get the name of this database.
*
* @return the database name
*/
@Override @NonNull
public String getName() {
return "MariaDB";
}
/**
* 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
* @see MariaDBAuthorization for credentials
*/
@Override
public void connect(MariaDBAuthorization credentials) throws IllegalArgumentException, IllegalStateException {
// if (credentials == null) { // We need valid credentials
// throw new IllegalArgumentException("No credentials defined");
// }
// if (isConnected()) { // Already connected
// throw new IllegalStateException("Already connected");
// }
// if (dataSource != null) { // We have a data source, close it first
// dataSource.close();
// }
// HikariConfig config = credentials.getHikariConfig(); // Get the custom config
// if (config == null) { // No custom config, make a new one
// config = new HikariConfig();
// }
// config.setJdbcUrl(credentials.getJdbcUrl()); // Set the JDBC connection URL
// config.setUsername(credentials.getUsername()); // Set the username
// config.setPassword(credentials.getPassword()); // Set the password
// dataSource = new HikariDataSource(config); // Create a new data source
throw new UnsupportedOperationException(); // Not impl yet
}
/**
* Check if this database is connected.
*
* @return the database connection state
*/
@Override
public boolean isConnected() {
return dataSource != null && (dataSource.isRunning() && !dataSource.isClosed());
}
/**
* Get the latency to this database.
*
* @return the latency, -1 if not connected
*/
@Override
public long getLatency() {
return 0;
}
/**
* Get the bootstrap class
* instance for this database.
*
* @return the bootstrap class instance, null if none
* @see HikariDataSource for bootstrap class
*/
@Override
public HikariDataSource getBootstrap() {
return dataSource;
}
/**
* 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 (dataSource != null) {
dataSource.close();
}
dataSource = null;
}
}

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.mariadb;
import com.zaxxer.hikari.HikariConfig;
import lombok.*;
/**
* Authorization for a {@link MariaDB} database.
*
* @author Braydon
*/
@RequiredArgsConstructor @AllArgsConstructor @Getter @ToString
public final class MariaDBAuthorization {
/**
* The JDBC connection URL for the database.
*/
@NonNull private final String jdbcUrl;
/**
* The username to use for authenticating with the database.
*/
@NonNull private final String username;
/**
* The password to use for authenticating with the database.
*/
@NonNull private final String password;
/**
* The optional {@link HikariConfig} to use.
*/
private HikariConfig hikariConfig;
}

@ -0,0 +1,93 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.mariadb;
import lombok.NonNull;
import me.braydon.feather.database.Repository;
import java.util.List;
/**
* The {@link MariaDB} {@link Repository} implementation.
*
* @author Braydon
* @param <ID> the identifier for type for entities
* @param <E> the entity type this repository stores
*/
public class MariaDBRepository<ID, E> extends Repository<MariaDB, ID, E> {
public MariaDBRepository(@NonNull MariaDB database, @NonNull Class<? extends E> entityClass) {
super(database, entityClass);
}
/**
* Get the entity with the given id.
*
* @param id the entity id
* @return the entity with the id, null if none
* @see ID for id
* @see E for entity
*/
@Override
public E find(@NonNull ID id) {
throw new UnsupportedOperationException();
}
/**
* Get all entities within this repository.
*
* @return the entities
* @see E for entity
*/
@Override
public List<E> findAll() {
throw new UnsupportedOperationException();
}
/**
* Save the given entities.
*
* @param entities the entities to save
* @see E for entity
*/
@Override
public void saveAll(@NonNull E... entities) {
throw new UnsupportedOperationException();
}
/**
* Get the amount of stored entities.
*
* @return the amount of stored entities
* @see E for entity
*/
@Override
public long count() {
throw new UnsupportedOperationException();
}
/**
* Drop the entity with the given id.
*
* @param id the entity id to drop
* @see ID for id
* @see E for entity
*/
@Override
public void dropById(@NonNull ID id) {
throw new UnsupportedOperationException();
}
/**
* Drop the given entity.
*
* @param entity the entity to drop
* @see E for entity
*/
@Override
public void drop(@NonNull E entity) {
throw new UnsupportedOperationException();
}
}

@ -0,0 +1,80 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.mariadb.queries;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Singular;
import me.braydon.feather.database.impl.mariadb.MariaDB;
import java.util.Map;
/**
* A builder for {@link MariaDB} update queries.
*
* @author Braydon
*/
@Builder @Getter
public class UpdateQuery {
/**
* The table to execute the update in.
*/
@NonNull private String table;
/**
* The mapped values (by column) for this query.
*/
@NonNull @Singular private Map<String, Object> values;
/**
* The where clause for this query.
*/
@NonNull private WhereClause whereClause;
/**
* Build this query.
*
* @return the built query
*/
@Override @NonNull
public String toString() {
// Build the values
StringBuilder valuesBuilder = new StringBuilder();
for (Map.Entry<String, Object> entry : values.entrySet()) {
valuesBuilder.append("`").append(entry.getKey()).append("`")
.append("='").append(entry.getValue()).append("', ");
}
String values = valuesBuilder.toString(); // The built values string
values = values.substring(0, values.length() - 2); // Remove the trailing comma
// Build the query
return String.format("UPDATE `%s` SET %s WHERE %s;",
table, values, whereClause
);
}
/**
* The where clause for this query.
*/
@Builder @Getter
public static class WhereClause {
/**
* The column to target.
*/
@NonNull private String column;
/**
* The value of the column to target.
*/
@NonNull private Object value;
@Override @NonNull
public String toString() {
return column + "='" + value + "'";
}
}
}

@ -49,6 +49,7 @@ public class MongoDB implements IDatabase<MongoClient, ConnectionString> {
* @param credentials the optional credentials to use * @param credentials the optional credentials to use
* @throws IllegalArgumentException if no credentials or database name is provided * @throws IllegalArgumentException if no credentials or database name is provided
* @throws IllegalStateException if already connected * @throws IllegalStateException if already connected
* @see ConnectionString for credentials
*/ */
@Override @Override
public void connect(ConnectionString credentials) throws IllegalArgumentException, IllegalStateException { public void connect(ConnectionString credentials) throws IllegalArgumentException, IllegalStateException {
@ -91,8 +92,8 @@ public class MongoDB implements IDatabase<MongoClient, ConnectionString> {
} }
// Return ping // Return ping
long before = System.currentTimeMillis(); long before = System.currentTimeMillis();
database.runCommand(new BasicDBObject("ping", "1")); database.runCommand(new BasicDBObject("ping", "1")); // Send a ping command
return System.currentTimeMillis() - before; return System.currentTimeMillis() - before; // Return time difference
} }
/** /**
@ -123,7 +124,7 @@ public class MongoDB implements IDatabase<MongoClient, ConnectionString> {
if (!isConnected()) { // Not connected if (!isConnected()) { // Not connected
throw new IllegalStateException("Not connected"); throw new IllegalStateException("Not connected");
} }
return new MongoRepository<>(this, entityClass, database.getCollection(collectionName)); return new MongoRepository<>(this, entityClass, collectionName);
} }
/** /**

@ -12,16 +12,16 @@ import com.mongodb.client.model.Indexes;
import com.mongodb.client.model.UpdateOneModel; import com.mongodb.client.model.UpdateOneModel;
import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.UpdateOptions;
import lombok.NonNull; import lombok.NonNull;
import lombok.SneakyThrows;
import me.braydon.feather.common.Tuple; import me.braydon.feather.common.Tuple;
import me.braydon.feather.database.Repository; import me.braydon.feather.database.Repository;
import me.braydon.feather.database.impl.mongodb.annotation.CustomDocument;
import me.braydon.feather.database.impl.mongodb.annotation.Index; import me.braydon.feather.database.impl.mongodb.annotation.Index;
import org.bson.Document; import org.bson.Document;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.lang.reflect.Method;
import java.util.Collections; import java.util.*;
import java.util.List;
import java.util.Map;
/** /**
* The {@link MongoDB} {@link Repository} implementation. * The {@link MongoDB} {@link Repository} implementation.
@ -36,6 +36,10 @@ public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
*/ */
@NonNull private final MongoCollection<Document> collection; @NonNull private final MongoCollection<Document> collection;
public MongoRepository(@NonNull MongoDB database, @NonNull Class<? extends E> entityClass, @NonNull String collectionName) {
this(database, entityClass, database.getDatabase().getCollection(collectionName));
}
public MongoRepository(@NonNull MongoDB database, @NonNull Class<? extends E> entityClass, @NonNull MongoCollection<Document> collection) { public MongoRepository(@NonNull MongoDB database, @NonNull Class<? extends E> entityClass, @NonNull MongoCollection<Document> collection) {
super(database, entityClass); super(database, entityClass);
this.collection = collection; this.collection = collection;
@ -90,17 +94,29 @@ public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
* @param entities the entities to save * @param entities the entities to save
* @see E for entity * @see E for entity
*/ */
@Override @Override @SneakyThrows
public void saveAll(@NonNull E... entities) { public void saveAll(@NonNull E... entities) {
List<UpdateOneModel<Document>> updateModels = new ArrayList<>(); // The update models to bulk write List<UpdateOneModel<Document>> updateModels = new ArrayList<>(); // The update models to bulk write
for (E entity : entities) { for (E entity : entities) {
me.braydon.feather.data.Document<Object> document = new me.braydon.feather.data.Document<>(entity); // Create a document from the entity me.braydon.feather.data.Document document = new me.braydon.feather.data.Document(entity); // Create a document from the entity
Document bsonDocument; // The Bson document to save
Method customDocumentMethod = Arrays.stream(entity.getClass().getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(CustomDocument.class))
.findFirst().orElse(null); // Get the @CustomDocument method
// We have a custom document method
if (customDocumentMethod != null && (customDocumentMethod.getReturnType() == Document.class)) {
bsonDocument = (Document) customDocumentMethod.invoke(entity); // Get our custom document
} else { // Otherwise, use our mapped data
bsonDocument = new Document(document.toMappedData());
}
// Add our update model to the list // Add our update model to the list
updateModels.add(new UpdateOneModel<>( updateModels.add(new UpdateOneModel<>(
Filters.eq(document.getIdKey(), document.getKey()), Filters.eq(document.getIdKey(), document.getKey()),
new Document("$set", new Document(document.toMappedData())), new Document("$set", bsonDocument),
new UpdateOptions().upsert(true) new UpdateOptions().upsert(true)
)); ));
@ -162,7 +178,7 @@ public class MongoRepository<ID, E> extends Repository<MongoDB, ID, E> {
*/ */
@Override @Override
public void drop(@NonNull E entity) { public void drop(@NonNull E entity) {
me.braydon.feather.data.Document<Object> document = new me.braydon.feather.data.Document<>(entity); // Create a document from the entity me.braydon.feather.data.Document 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 collection.deleteOne(new Document(document.getIdKey(), document.getKey())); // Delete the entity
} }
} }

@ -0,0 +1,21 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.mongodb.annotation;
import java.lang.annotation.*;
/**
* Methods tagged with this annotation will be invoked
* when a document is being saved to the database, this
* allows is to save a custom document, rather than
* parsing the entity and creating one that way.
*
* @author Braydon
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented @Inherited
public @interface CustomDocument { }

@ -19,7 +19,7 @@ import me.braydon.feather.database.IDatabase;
* @see RedisURI for the credentials class * @see RedisURI for the credentials class
* @see <a href="https://redis.io">Redis Official Site</a> * @see <a href="https://redis.io">Redis Official Site</a>
*/ */
public class Redis implements IDatabase<StatefulRedisConnection<String, String>, RedisURI> { public class Redis implements IDatabase<StatefulRedisConnection<String, String>, String> {
/** /**
* The current {@link RedisClient} instance. * The current {@link RedisClient} instance.
*/ */
@ -43,25 +43,25 @@ public class Redis implements IDatabase<StatefulRedisConnection<String, String>,
/** /**
* Initialize a connection to this database. * Initialize a connection to this database.
* *
* @param credentials the optional credentials to use * @param uri the optional credentials to use
* @throws IllegalArgumentException if no credentials are provided * @throws IllegalArgumentException if no credentials are provided
* @throws IllegalStateException if already connected * @throws IllegalStateException if already connected
*/ */
@Override @Override
public void connect(RedisURI credentials) throws IllegalArgumentException, IllegalStateException { public void connect(String uri) throws IllegalArgumentException, IllegalStateException {
if (credentials == null) { // We need valid credentials if (uri == null) { // We need valid credentials
throw new IllegalArgumentException("No credentials defined"); throw new IllegalArgumentException("No credentials defined");
} }
if (isConnected()) { // Already connected if (isConnected()) { // Already connected
throw new IllegalStateException("Already connected"); throw new IllegalStateException("Already connected");
} }
if (client != null) { // We have a client, close it first if (client != null) { // We have a client, close it first
client.close(); client.shutdown();
} }
if (connection != null) { // We have a connection, close it first if (connection != null) { // We have a connection, close it first
connection.close(); connection.close();
} }
client = RedisClient.create(credentials); // Create a new client client = RedisClient.create(uri); // Create a new client
connection = client.connect(); // Connect to the Redis server connection = client.connect(); // Connect to the Redis server
} }
@ -82,7 +82,13 @@ public class Redis implements IDatabase<StatefulRedisConnection<String, String>,
*/ */
@Override @Override
public long getLatency() { public long getLatency() {
return 0; if (!isConnected()) { // Not connected
return -1L;
}
// Return ping
long before = System.currentTimeMillis();
connection.sync().ping(); // Send a ping command
return System.currentTimeMillis() - before; // Return time difference
} }
/** /**
@ -139,7 +145,7 @@ public class Redis implements IDatabase<StatefulRedisConnection<String, String>,
@Override @Override
public void close() { public void close() {
if (client != null) { if (client != null) {
client.close(); client.shutdown();
} }
if (connection != null) { if (connection != null) {
connection.close(); connection.close();

@ -7,12 +7,13 @@ package me.braydon.feather.database.impl.redis;
import io.lettuce.core.api.sync.RedisCommands; import io.lettuce.core.api.sync.RedisCommands;
import lombok.NonNull; import lombok.NonNull;
import me.braydon.feather.common.Tuple;
import me.braydon.feather.data.Document; import me.braydon.feather.data.Document;
import me.braydon.feather.database.Repository; import me.braydon.feather.database.Repository;
import me.braydon.feather.database.impl.redis.annotation.TTL;
import java.util.ArrayList; import java.lang.reflect.Field;
import java.util.Collections; import java.util.*;
import java.util.List;
/** /**
* The {@link Redis} {@link Repository} implementation. * The {@link Redis} {@link Repository} implementation.
@ -78,8 +79,24 @@ public class RedisRepository<ID, E> extends Repository<Redis, ID, E> {
commands.multi(); commands.multi();
} }
for (E entity : entities) { // Set our entities for (E entity : entities) { // Set our entities
Document<String> document = new Document<>(entity); // Create a document from the entity Document document = new Document(entity); // Create a document from the entity
commands.hmset(keyPrefix + ":" + document.getKey(), document.toMappedData()); String key = keyPrefix + ":" + document.getKey(); // The key of this entity
Map<String, String> mappedData = new HashMap<>();
for (Map.Entry<String, Tuple<Field, Object>> entry : document.getMappedData().entrySet()) {
mappedData.put(entry.getKey(), String.valueOf(entry.getValue().getRight()));
}
commands.hmset(key, mappedData); // Set the mapped document in the database
// Handling @TTL annotations
Class<?> clazz = entity.getClass(); // The entity class
if (!clazz.isAnnotationPresent(TTL.class)) { // Missing @TTL
continue;
}
long ttl = clazz.getAnnotation(TTL.class).value(); // Get the ttl value
if (ttl > 0L) { // Value is above zero, set it
commands.expire(key, ttl);
}
} }
if (multi) { // Execute the commands in bulk if (multi) { // Execute the commands in bulk
commands.exec(); commands.exec();
@ -117,7 +134,7 @@ public class RedisRepository<ID, E> extends Repository<Redis, ID, E> {
*/ */
@Override @Override
public void drop(@NonNull E entity) { public void drop(@NonNull E entity) {
me.braydon.feather.data.Document<Object> document = new me.braydon.feather.data.Document<>(entity); // Create a document from the entity me.braydon.feather.data.Document document = new me.braydon.feather.data.Document(entity); // Create a document from the entity
getDatabase().getBootstrap().sync().del(keyPrefix + ":" + document.getKey()); getDatabase().getBootstrap().sync().del(keyPrefix + ":" + document.getKey());
} }
} }

@ -0,0 +1,35 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.feather.database.impl.redis.annotation;
import java.lang.annotation.*;
/**
* Entities tagged with this annotation will
* expire after the amount of defined seconds.
*
* @author Braydon
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented @Inherited
public @interface TTL {
/**
* The time-to-live value.
* <p>
* If the value is zero or
* below, then there will
* be no expiration.
* <p>
* The key this TTL rule is assigned
* to will expire in the defined amount
* of seconds from when the key was updated.
* </p>
*
* @return the value in seconds
*/
long value() default -1L;
}