diff --git a/pom.xml b/pom.xml
index 8d9a6f9..b75560b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -227,5 +227,11 @@
4.13.2
test
+
+ org.slf4j
+ slf4j-simple
+ 2.0.16
+ test
+
\ No newline at end of file
diff --git a/src/main/java/wtf/casper/storageapi/Filter.java b/src/main/java/wtf/casper/storageapi/Filter.java
new file mode 100644
index 0000000..7525c1a
--- /dev/null
+++ b/src/main/java/wtf/casper/storageapi/Filter.java
@@ -0,0 +1,49 @@
+package wtf.casper.storageapi;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public record Filter(String key, Object value, FilterType filterType, SortingType sortingType, Type type) {
+ public static Filter of(String key, Object value, FilterType filterType, SortingType sortingType, Type type) {
+ return new Filter(key, value, filterType, sortingType, type);
+ }
+
+ public static Filter of(String key, Object value, FilterType filterType, SortingType sortingType) {
+ return new Filter(key, value, filterType, sortingType, Type.AND);
+ }
+
+ public static Filter of(String key, Object value, FilterType filterType) {
+ return new Filter(key, value, filterType, SortingType.NONE, Type.AND);
+ }
+
+ public enum Type {
+ AND,
+ OR
+ }
+
+ /**
+ * Groups filters into a list of lists, where each list is a group of filters that are connected by OR
+ * [AND, OR, AND, AND, OR, AND] -> [[AND] OR [AND, AND] OR [AND]]
+ * @param filters the filters to group
+ * @return the grouped filters
+ */
+ public static List> group(Filter... filters) {
+ List> groups = new ArrayList<>();
+ groups.add(new ArrayList<>());
+ for (Filter filter : filters) {
+ List lastGroup = groups.get(groups.size() - 1);
+ if (filter.type() == Type.AND) {
+ lastGroup.add(filter);
+ continue;
+ }
+
+ if (!lastGroup.isEmpty()) {
+ groups.add(new ArrayList<>());
+ lastGroup = groups.get(groups.size() - 1);
+ }
+ lastGroup.add(filter);
+ }
+
+ return groups;
+ }
+}
diff --git a/src/main/java/wtf/casper/storageapi/StatelessFieldStorage.java b/src/main/java/wtf/casper/storageapi/StatelessFieldStorage.java
new file mode 100644
index 0000000..c1cc607
--- /dev/null
+++ b/src/main/java/wtf/casper/storageapi/StatelessFieldStorage.java
@@ -0,0 +1,252 @@
+package wtf.casper.storageapi;
+
+import dev.dejvokep.boostedyaml.YamlDocument;
+import dev.dejvokep.boostedyaml.block.implementation.Section;
+import wtf.casper.storageapi.misc.ConstructableValue;
+import wtf.casper.storageapi.misc.KeyValue;
+import wtf.casper.storageapi.utils.ReflectionUtil;
+import wtf.casper.storageapi.utils.StorageAPIConstants;
+
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+
+public interface StatelessFieldStorage {
+
+ /**
+ * @param field the field to search for.
+ * @param value the value to search for.
+ * @return a future that will complete with a collection of all values that match the given field and value.
+ */
+ default CompletableFuture> get(final String field, final Object value) {
+ return get(field, value, FilterType.EQUALS, SortingType.NONE);
+ }
+
+ /**
+ * @param field the field to search for.
+ * @param value the value to search for.
+ * @param filterType the filter type to use.
+ * @param sortingType the sorting type to use.
+ * @return a future that will complete with a collection of all values that match the given field and value.
+ */
+ default CompletableFuture> get(final String field, final Object value, final FilterType filterType, final SortingType sortingType) {
+ return get(Filter.of(field, value, filterType, sortingType));
+ };
+
+ /**
+ * @param filters the filters to use.
+ * @return a future that will complete with a collection of all value that match the given filters.
+ */
+ default CompletableFuture> get(Filter... filters) {
+ return get(-1, filters);
+ };
+
+ /**
+ * @param limit the limit of values to return.
+ * @param filters the filters to use.
+ * @return a future that will complete with a collection of all value that match the given filters.
+ */
+ CompletableFuture> get(int limit, Filter... filters);
+
+ /**
+ * @param key the key to search for.
+ * @return a future that will complete with the value that matches the given key.
+ * The value may be null if the key is not found.
+ */
+ CompletableFuture get(final K key);
+
+ /**
+ * @param key the key to search for.
+ * @return a future that will complete with the value that matches the given key or a generated value if not found.
+ */
+ default CompletableFuture getOrDefault(final K key) {
+ return get(key).thenApply((v) -> {
+
+ if (v != null) {
+ return v;
+ }
+
+ if (this instanceof ConstructableValue, ?>) {
+ v = ((ConstructableValue) this).constructValue(key);
+ if (v == null) {
+ throw new RuntimeException("Failed to create default value for V with key " + key + ". " +
+ "Please create a constructor in V for only the key");
+ }
+ return v;
+ }
+
+ if (this instanceof KeyValue, ?>) {
+ KeyValue keyValueGetter = (KeyValue) this;
+ try {
+ return ReflectionUtil.createInstance(keyValueGetter.value(), key);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to create default value for V with key " + key + ". " +
+ "Please create a constructor in V for only the key.", e);
+ }
+ }
+
+ try {
+ if (getClass().getGenericSuperclass() instanceof ParameterizedType parameterizedType) {
+ Type type = parameterizedType.getActualTypeArguments()[1];
+ Class aClass = (Class) Class.forName(type.getTypeName());
+ return ReflectionUtil.createInstance(aClass, key);
+ }
+
+ throw new RuntimeException("Failed to create default value for V with key " + key + ". " +
+ "Please create a constructor in V for only the key.");
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to create default value for V with key " + key + ". " +
+ "Please create a constructor in V for only the key.");
+ }
+ });
+ }
+
+ /**
+ * @param field the field to search for.
+ * @param value the value to search for.
+ * @return a future that will complete with the first value that matches the given field and value.
+ */
+ default CompletableFuture getFirst(final String field, final Object value) {
+ return getFirst(field, value, FilterType.EQUALS);
+ }
+
+ /**
+ * @param field the field to search for.
+ * @param value the value to search for.
+ * @param filterType the filter type to use.
+ * @return a future that will complete with the first value that matches the given field and value.
+ */
+ default CompletableFuture getFirst(final String field, final Object value, FilterType filterType) {
+ return get(1, Filter.of(field, value, filterType, SortingType.NONE)).thenApply((values) -> {
+ if (values.isEmpty()) {
+ return null;
+ }
+
+ return values.iterator().next();
+ });
+ };
+
+
+ /**
+ * @param value the value to save.
+ */
+ CompletableFuture save(final V value);
+
+ /**
+ * @param values the values to save.
+ */
+ default CompletableFuture saveAll(final Collection values) {
+ return CompletableFuture.runAsync(() -> values.forEach(v -> save(v).join()), StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ /**
+ * @param key the key to remove.
+ */
+ CompletableFuture remove(final V key);
+
+ /**
+ * Writes the storage to disk.
+ */
+ CompletableFuture write();
+
+ /**
+ * Deletes the storage from disk.
+ */
+ CompletableFuture deleteAll();
+
+ /**
+ * Closes the storage/storage connection.
+ */
+ default CompletableFuture close() {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ /**
+ * @param field the field to search for.
+ * @param value the value to search for.
+ * @return a future that will complete with a boolean that represents whether the storage contains a value that matches the given field and value.
+ */
+ CompletableFuture contains(final String field, final Object value);
+
+ /**
+ * @param storage the storage to migrate from. The data will be copied from the given storage to this storage.
+ * @return a future that will complete with a boolean that represents whether the migration was successful.
+ */
+ default CompletableFuture migrate(final StatelessFieldStorage storage) {
+ return CompletableFuture.supplyAsync(() -> {
+ storage.allValues().thenAccept((values) -> values.forEach(v -> save(v).join())).join();
+ return true;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ /**
+ * @param oldStorageSupplier supplier to provide the old storage
+ * @param config the config
+ * @param path the path to the storage
+ * @return a future that will complete with a boolean that represents whether the migration was successful.
+ */
+ default CompletableFuture migrateFrom(Supplier> oldStorageSupplier, YamlDocument config, String path) {
+ return CompletableFuture.supplyAsync(() -> {
+ if (config == null) return false;
+ Section section = config.getSection(path);
+ if (section == null) return false;
+ if (!section.getBoolean("migrate", false)) return false;
+ section.set("migrate", false);
+ try {
+ config.save();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // storage that we are migrating to the new storage
+ StatelessFieldStorage oldStorage = oldStorageSupplier.get();
+ try {
+ this.migrate(oldStorage).join();
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ /**
+ * @return a future that will complete with a collection of all values in the storage.
+ */
+ CompletableFuture> allValues();
+
+ /**
+ * @param field the field to search for.
+ * @param sortingType the sorting type to use.
+ * @return a future that will complete with a collection of all values in the storage that match the given field and value.
+ */
+ default CompletableFuture> allValues(String field, SortingType sortingType) {
+ return CompletableFuture.supplyAsync(() -> {
+ Collection values = allValues().join();
+ if (values.isEmpty()) {
+ return values;
+ }
+
+ // Sort the values.
+ return sortingType.sort(values, field);
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ /**
+ * Adds an index to the storage.
+ * @param field the field to add an index for.
+ * @return a future that will complete when the index has been added.
+ */
+ CompletableFuture addIndex(String field);
+
+ /**
+ * Removes an index from the storage.
+ * @param field the field to remove the index for.
+ * @return a future that will complete when the index has been removed.
+ */
+ CompletableFuture removeIndex(String field);
+}
diff --git a/src/main/java/wtf/casper/storageapi/StorageType.java b/src/main/java/wtf/casper/storageapi/StorageType.java
index 2b81731..2007325 100644
--- a/src/main/java/wtf/casper/storageapi/StorageType.java
+++ b/src/main/java/wtf/casper/storageapi/StorageType.java
@@ -1,5 +1,18 @@
package wtf.casper.storageapi;
+/*
+ * TODO:
+ * - cubrid
+ * - db2
+ * - derby
+ * - firebird
+ * - h2
+ * - hsqldb
+ * - oracle
+ * - postgresql
+ * - mssql
+ * - hql (hibernate)
+ */
public enum StorageType {
JSON,
SQL,
diff --git a/src/main/java/wtf/casper/storageapi/impl/direct/statelessfstorage/DirectStatelessMariaDBFStorage.java b/src/main/java/wtf/casper/storageapi/impl/direct/statelessfstorage/DirectStatelessMariaDBFStorage.java
new file mode 100644
index 0000000..66969d1
--- /dev/null
+++ b/src/main/java/wtf/casper/storageapi/impl/direct/statelessfstorage/DirectStatelessMariaDBFStorage.java
@@ -0,0 +1,23 @@
+package wtf.casper.storageapi.impl.direct.statelessfstorage;
+
+import wtf.casper.storageapi.Credentials;
+import wtf.casper.storageapi.impl.statelessfstorage.StatelessMariaDBFStorage;
+import wtf.casper.storageapi.impl.statelessfstorage.StatelessMongoFStorage;
+import wtf.casper.storageapi.misc.ConstructableValue;
+
+import java.util.function.Function;
+
+public class DirectStatelessMariaDBFStorage extends StatelessMariaDBFStorage implements ConstructableValue {
+
+ private final Function function;
+
+ public DirectStatelessMariaDBFStorage(Class keyClass, Class valueClass, Credentials credentials, Function function) {
+ super(keyClass, valueClass, credentials);
+ this.function = function;
+ }
+
+ @Override
+ public V constructValue(K key) {
+ return function.apply(key);
+ }
+}
diff --git a/src/main/java/wtf/casper/storageapi/impl/fstorage/JsonFStorage.java b/src/main/java/wtf/casper/storageapi/impl/fstorage/JsonFStorage.java
new file mode 100644
index 0000000..b2f0a73
--- /dev/null
+++ b/src/main/java/wtf/casper/storageapi/impl/fstorage/JsonFStorage.java
@@ -0,0 +1,195 @@
+package wtf.casper.storageapi.impl.fstorage;
+
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import lombok.SneakyThrows;
+import org.jetbrains.annotations.Nullable;
+import wtf.casper.storageapi.FieldStorage;
+import wtf.casper.storageapi.Filter;
+import wtf.casper.storageapi.FilterType;
+import wtf.casper.storageapi.SortingType;
+import wtf.casper.storageapi.cache.Cache;
+import wtf.casper.storageapi.cache.CaffeineCache;
+import wtf.casper.storageapi.cache.MapCache;
+import wtf.casper.storageapi.id.utils.IdUtils;
+import wtf.casper.storageapi.misc.ConstructableValue;
+import wtf.casper.storageapi.utils.StorageAPIConstants;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+public abstract class JsonFStorage implements FieldStorage, ConstructableValue {
+
+ private final File file;
+ private final Class keyClass;
+ private final Class valueClass;
+ private Cache cache = new MapCache<>(new HashMap<>());
+
+ @SneakyThrows
+ public JsonFStorage(final File file, final Class keyClass, final Class valueClass) {
+ if (file == null) {
+ throw new IllegalArgumentException("File cannot be null");
+ }
+
+ if (!file.exists()) {
+ file.getParentFile().mkdirs();
+ if (!file.createNewFile()) {
+ throw new RuntimeException("Failed to create file " + file.getAbsolutePath());
+ }
+ }
+
+ this.file = file;
+ this.valueClass = valueClass;
+ this.keyClass = keyClass;
+
+ final V[] values = StorageAPIConstants.getGson().fromJson(new FileReader(file), (Class) Array.newInstance(valueClass, 0).getClass());
+
+ if (values != null) {
+ for (final V value : values) {
+ this.cache.put((K) IdUtils.getId(valueClass, value), value);
+ }
+ }
+ }
+
+ @Override
+ public Class key() {
+ return keyClass;
+ }
+
+ @Override
+ public Class value() {
+ return valueClass;
+ }
+
+ @Override
+ public Cache cache() {
+ return cache;
+ }
+
+ @Override
+ public void cache(Cache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public CompletableFuture deleteAll() {
+ return CompletableFuture.runAsync(() -> {
+ this.cache.invalidateAll();
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture> get(String field, Object value, FilterType filterType, SortingType sortingType) {
+ return CompletableFuture.supplyAsync(() -> {
+ Collection values = cache().asMap().values();
+ return sortingType.sort(filter(values, field, value, filterType), field);
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture get(K key) {
+ return CompletableFuture.supplyAsync(() -> cache.getIfPresent(key));
+ }
+
+ @Override
+ public CompletableFuture> get(int limit, Filter... filters) {
+ return CompletableFuture.supplyAsync(() -> {
+ Collection values = cache().asMap().values();
+ for (Filter filter : filters) {
+ values = filter(values, filter.key(), filter.value(), filter.filterType());
+ }
+ return values;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture contains(String field, Object value) {
+ return CompletableFuture.supplyAsync(() -> {
+ Collection values = cache().asMap().values();
+ return !filter(values, field, value, FilterType.EQUALS).isEmpty();
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture getFirst(String field, Object value, FilterType filterType) {
+ return CompletableFuture.supplyAsync(() -> {
+ Collection values = cache().asMap().values();
+ return filterFirst(values, field, value, filterType);
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture save(V value) {
+ return CompletableFuture.runAsync(() -> {
+ cache.put((K) IdUtils.getId(valueClass, value), value);
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture remove(V value) {
+ return CompletableFuture.runAsync(() -> {
+ cache.invalidate((K) IdUtils.getId(valueClass, value));
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture write() {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ boolean delete = this.file.delete();
+ if (!delete) {
+ System.out.println("Failed to delete file " + this.file.getAbsolutePath());
+ }
+ this.file.createNewFile();
+
+ final Writer writer = new FileWriter(this.file);
+
+ StorageAPIConstants.getGson().toJson(this.cache.asMap().values(), writer);
+ writer.close();
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture> allValues() {
+ return CompletableFuture.supplyAsync(() -> cache.asMap().values(), StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture addIndex(String field) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ @Override
+ public CompletableFuture removeIndex(String field) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ private Collection filter(final Collection values, final String field, final Object value, FilterType filterType) {
+ List list = new ArrayList<>();
+ for (final V v : values) {
+ if (filterType.passes(v, field, value)) {
+ list.add(v);
+ }
+ }
+ return list;
+ }
+
+ @Nullable
+ private V filterFirst(final Collection values, final String field, final Object value, FilterType filterType) {
+ for (final V v : values) {
+ if (filterType.passes(v, field, value)) return v;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/wtf/casper/storageapi/impl/fstorage/MongoFStorage.java b/src/main/java/wtf/casper/storageapi/impl/fstorage/MongoFStorage.java
new file mode 100644
index 0000000..e9b7979
--- /dev/null
+++ b/src/main/java/wtf/casper/storageapi/impl/fstorage/MongoFStorage.java
@@ -0,0 +1,139 @@
+package wtf.casper.storageapi.impl.fstorage;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.mongodb.MongoClient;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.ReplaceOptions;
+import lombok.Getter;
+import lombok.extern.java.Log;
+import org.bson.Document;
+import org.bson.conversions.Bson;
+import wtf.casper.storageapi.*;
+import wtf.casper.storageapi.cache.Cache;
+import wtf.casper.storageapi.cache.CaffeineCache;
+import wtf.casper.storageapi.id.utils.IdUtils;
+import wtf.casper.storageapi.impl.statelessfstorage.StatelessMongoFStorage;
+import wtf.casper.storageapi.misc.ConstructableValue;
+import wtf.casper.storageapi.misc.IMongoStorage;
+import wtf.casper.storageapi.misc.MongoProvider;
+import wtf.casper.storageapi.utils.StorageAPIConstants;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@Log
+public abstract class MongoFStorage extends StatelessMongoFStorage implements FieldStorage, ConstructableValue, IMongoStorage {
+
+ private Cache cache = new CaffeineCache<>(Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build());
+
+ public MongoFStorage(final Class keyClass, final Class valueClass, final Credentials credentials) {
+ this(credentials.getUri(), credentials.getDatabase(), credentials.getCollection(), keyClass, valueClass);
+ }
+
+ public MongoFStorage(final String uri, final String database, final String collection, final Class keyClass, final Class valueClass) {
+ super(uri, database, collection, keyClass, valueClass);
+ }
+
+ @Override
+ public Cache cache() {
+ return cache;
+ }
+
+ @Override
+ public void cache(Cache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public CompletableFuture> get(String field, Object value, FilterType filterType, SortingType sortingType) {
+ return CompletableFuture.supplyAsync(() -> {
+
+ Collection collection = new ArrayList<>();
+ Bson filter = getDocument(filterType, field, value);
+ List into = getCollection().find(filter).into(new ArrayList<>());
+
+ for (Document document : into) {
+ V obj = StorageAPIConstants.getGson().fromJson(document.toJson(StorageAPIConstants.getJsonWriterSettings()), valueClass);
+ collection.add(obj);
+ }
+
+ return sortingType.sort(collection, field);
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture get(K key) {
+ return CompletableFuture.supplyAsync(() -> {
+ if (cache.getIfPresent(key) != null) {
+ return cache.getIfPresent(key);
+ }
+ V join = super.get(key).join();
+ cache.asMap().putIfAbsent(key, join);
+ return join;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture getFirst(String field, Object value, FilterType filterType) {
+ return CompletableFuture.supplyAsync(() -> {
+ if (!cache.asMap().isEmpty()) {
+ for (V v : cache.asMap().values()) {
+ if (filterType.passes(v, field, value)) {
+ return v;
+ }
+ }
+ }
+
+ V obj = super.getFirst(field, value, filterType).join();
+ cache.asMap().putIfAbsent((K) IdUtils.getId(obj), obj);
+
+ return obj;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture save(V value) {
+ return CompletableFuture.runAsync(() -> {
+ K key = (K) IdUtils.getId(valueClass, value);
+ cache.asMap().putIfAbsent(key, value);
+ super.save(value).join();
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture saveAll(Collection values) {
+ return CompletableFuture.runAsync(() -> {
+ for (V value : values) {
+ K key = (K) IdUtils.getId(valueClass, value);
+ cache.asMap().putIfAbsent(key, value);
+ }
+ super.saveAll(values).join();
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture remove(V key) {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ K id = (K) IdUtils.getId(valueClass, key);
+ cache.invalidate(id);
+ getCollection().deleteMany(getDocument(FilterType.EQUALS, "_id", convertUUIDtoString(id)));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture close() {
+ return CompletableFuture.supplyAsync(() -> {
+ cache.invalidateAll();
+ MongoProvider.closeClient(uri);
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+}
diff --git a/src/main/java/wtf/casper/storageapi/impl/statelessfstorage/StatelessMariaDBFStorage.java b/src/main/java/wtf/casper/storageapi/impl/statelessfstorage/StatelessMariaDBFStorage.java
new file mode 100644
index 0000000..67f5f75
--- /dev/null
+++ b/src/main/java/wtf/casper/storageapi/impl/statelessfstorage/StatelessMariaDBFStorage.java
@@ -0,0 +1,284 @@
+package wtf.casper.storageapi.impl.statelessfstorage;
+
+import com.zaxxer.hikari.HikariDataSource;
+import lombok.extern.slf4j.Slf4j;
+import org.intellij.lang.annotations.Language;
+import wtf.casper.storageapi.Credentials;
+import wtf.casper.storageapi.Filter;
+import wtf.casper.storageapi.FilterType;
+import wtf.casper.storageapi.StatelessFieldStorage;
+import wtf.casper.storageapi.id.utils.IdUtils;
+import wtf.casper.storageapi.misc.ConstructableValue;
+import wtf.casper.storageapi.utils.StorageAPIConstants;
+
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+@Slf4j
+public class StatelessMariaDBFStorage implements StatelessFieldStorage, ConstructableValue {
+ private final Class keyClass;
+ private final Class valueClass;
+ private final String idFieldName;
+ private final HikariDataSource ds;
+ private final String table;
+
+ public StatelessMariaDBFStorage(final Class keyClass, final Class valueClass, Credentials credentials) {
+ this(keyClass, valueClass, credentials.getTable(), credentials.getHost(), credentials.getPort(-1), credentials.getDatabase(), credentials.getUsername(), credentials.getPassword());
+ }
+
+ public StatelessMariaDBFStorage(final Class keyClass, final Class valueClass, final String table, final String host, final int port, final String database, final String username, final String password) {
+ this.keyClass = keyClass;
+ this.valueClass = valueClass;
+ this.idFieldName = IdUtils.getIdName(this.valueClass);
+ this.ds = new HikariDataSource();
+ this.table = table;
+ this.ds.setMaximumPoolSize(20);
+ this.ds.setDriverClassName("org.mariadb.jdbc.Driver");
+ this.ds.setJdbcUrl("jdbc:mariadb://" + host + ":" + port + "/" + database + "?allowPublicKeyRetrieval=true&autoReconnect=true&useSSL=false");
+ this.ds.addDataSourceProperty("user", username);
+ this.ds.addDataSourceProperty("password", password);
+ this.ds.setAutoCommit(true);
+ createTable();
+ }
+
+ @Override
+ public Class key() {
+ return keyClass;
+ }
+
+ @Override
+ public Class value() {
+ return valueClass;
+ }
+
+ @Override
+ public CompletableFuture> get(int limit, Filter... filters) {
+ return CompletableFuture.supplyAsync(() -> {
+ List values = new ArrayList<>();
+ StringBuilder query = new StringBuilder("SELECT * FROM ").append(table).append(" WHERE ");
+
+ List> groups = Filter.group(filters);
+ for (List group : groups) {
+ query.append("(");
+ for (Filter filter : group) {
+ query.append("JSON_EXTRACT(data, '$.").append(filter.key()).append("') ").append(getSqlOperator(filter)).append(" AND ");
+ }
+ query.setLength(query.length() - 5); // Remove the last " AND "
+ query.append(") OR ");
+ }
+
+ query.setLength(query.length() - 4); // Remove the last " OR "
+
+ if (limit > 0) {
+ query.append(" LIMIT ").append(limit);
+ }
+
+ try (Connection connection = ds.getConnection();
+ PreparedStatement stmt = connection.prepareStatement(query.toString())) {
+ int index = 1;
+ for (Filter filter : filters) {
+ stmt.setObject(index++, filter.value());
+ }
+ ResultSet rs = stmt.executeQuery();
+ while (rs.next()) {
+ values.add(StorageAPIConstants.getGson().fromJson(rs.getString("data"), valueClass));
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ return values;
+
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture get(K key) {
+ return CompletableFuture.supplyAsync(() -> {
+ String query = "SELECT * FROM " + table + " WHERE " + idFieldName + " = ?";
+ try (Connection connection = ds.getConnection();
+ PreparedStatement stmt = connection.prepareStatement(query)) {
+ stmt.setObject(1, key);
+ ResultSet rs = stmt.executeQuery();
+ if (rs.next()) {
+ return StorageAPIConstants.getGson().fromJson(rs.getString("data"), valueClass);
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture save(V value) {
+ return CompletableFuture.supplyAsync(() -> {
+ String query = "REPLACE INTO " + table + " (" + idFieldName + ", data) VALUES (?, ?)";
+ try (Connection connection = ds.getConnection();
+ PreparedStatement stmt = connection.prepareStatement(query)) {
+ stmt.setObject(1, uuidToString(IdUtils.getId(valueClass, value)));
+ stmt.setString(2, StorageAPIConstants.getGson().toJson(value));
+ stmt.executeUpdate();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture remove(V key) {
+ return CompletableFuture.supplyAsync(() -> {
+ String query = "DELETE FROM " + table + " WHERE " + idFieldName + " = ?";
+ try (Connection connection = ds.getConnection();
+ PreparedStatement stmt = connection.prepareStatement(query)) {
+ stmt.setObject(1, uuidToString(IdUtils.getId(valueClass, key)));
+ stmt.executeUpdate();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture write() {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ @Override
+ public CompletableFuture deleteAll() {
+ return CompletableFuture.supplyAsync(() -> {
+ String query = "DELETE FROM " + table;
+ try (Connection connection = ds.getConnection();
+ PreparedStatement stmt = connection.prepareStatement(query)) {
+ stmt.executeUpdate();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture contains(String field, Object value) {
+ return CompletableFuture.supplyAsync(() -> {
+ String query = "SELECT 1 FROM " + table + " WHERE JSON_EXTRACT(data, '$." + field + "') = ?";
+ try (Connection connection = ds.getConnection();
+ PreparedStatement stmt = connection.prepareStatement(query)) {
+ stmt.setObject(1, value);
+ ResultSet rs = stmt.executeQuery();
+ return rs.next();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture> allValues() {
+ return CompletableFuture.supplyAsync(() -> {
+ List values = new ArrayList<>();
+ String query = "SELECT * FROM " + table;
+ try (Connection connection = ds.getConnection();
+ PreparedStatement stmt = connection.prepareStatement(query)) {
+ ResultSet rs = stmt.executeQuery();
+ while (rs.next()) {
+ values.add(StorageAPIConstants.getGson().fromJson(rs.getString("data"), valueClass));
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ return values;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture addIndex(String field) {
+ return CompletableFuture.runAsync(() -> {
+ String query = "ALTER TABLE " + table + " ADD COLUMN " + field + " TEXT AS (JSON_VALUE(data, '$." + field + "')) VIRTUAL";
+ try (Connection connection = ds.getConnection();
+ PreparedStatement stmt = connection.prepareStatement(query)) {
+ stmt.executeUpdate();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ @Override
+ public CompletableFuture removeIndex(String field) {
+ return CompletableFuture.runAsync(() -> {
+ String query = "ALTER TABLE " + table + " DROP COLUMN " + field;
+ try (Connection connection = ds.getConnection();
+ PreparedStatement stmt = connection.prepareStatement(query)) {
+ stmt.executeUpdate();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ private void createTable() {
+ try (Connection connection = ds.getConnection();
+ Statement stmt = connection.createStatement()) {
+ stmt.executeUpdate("CREATE TABLE IF NOT EXISTS " + table + " (" + idFieldName + " VARCHAR(255) PRIMARY KEY, data TEXT)");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private String getSqlOperator(Filter filter) {
+ switch (filter.filterType()) {
+ case ENDS_WITH -> {
+ return "LIKE CONCAT('\"%', ?, '\"')";
+ }
+ case NOT_ENDS_WITH -> {
+ return "NOT LIKE CONCAT('\"%', ?, '\"')";
+ }
+ case STARTS_WITH -> {
+ return "LIKE CONCAT('\"', ?, '%\"')";
+ }
+ case NOT_STARTS_WITH -> {
+ return "NOT LIKE CONCAT('\"', ?, '%\"')";
+ }
+ case CONTAINS -> {
+ return "LIKE CONCAT('%', ?, '%')";
+ }
+ case NOT_CONTAINS -> {
+ return "NOT LIKE CONCAT('%', ?, '%')";
+ }
+ case LESS_THAN, NOT_GREATER_THAN_OR_EQUAL_TO -> {
+ return "< ?";
+ }
+ case EQUALS -> {
+ return "= ?";
+ }
+ case GREATER_THAN, NOT_LESS_THAN_OR_EQUAL_TO -> {
+ return "> ?";
+ }
+ case LESS_THAN_OR_EQUAL_TO, NOT_GREATER_THAN -> {
+ return "<= ?";
+ }
+ case GREATER_THAN_OR_EQUAL_TO, NOT_LESS_THAN -> {
+ return ">= ?";
+ }
+ case NOT_EQUALS -> {
+ return "!= ?";
+ }
+ default -> throw new IllegalArgumentException("Unknown filter type: " + filter.filterType());
+ }
+ }
+
+ private Object uuidToString(Object uuid) {
+ if (uuid instanceof UUID) {
+ return uuid.toString();
+ } else {
+ return uuid;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/wtf/casper/storageapi/impl/statelessfstorage/StatelessMongoFStorage.java b/src/main/java/wtf/casper/storageapi/impl/statelessfstorage/StatelessMongoFStorage.java
new file mode 100644
index 0000000..c02a45a
--- /dev/null
+++ b/src/main/java/wtf/casper/storageapi/impl/statelessfstorage/StatelessMongoFStorage.java
@@ -0,0 +1,239 @@
+package wtf.casper.storageapi.impl.statelessfstorage;
+
+import com.mongodb.MongoClient;
+import com.mongodb.client.FindIterable;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.ReplaceOptions;
+import lombok.Getter;
+import lombok.extern.java.Log;
+import org.bson.BsonArray;
+import org.bson.Document;
+import wtf.casper.storageapi.*;
+import wtf.casper.storageapi.id.utils.IdUtils;
+import wtf.casper.storageapi.misc.ConstructableValue;
+import wtf.casper.storageapi.misc.IMongoStorage;
+import wtf.casper.storageapi.misc.MongoProvider;
+import wtf.casper.storageapi.utils.StorageAPIConstants;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+@Log
+public abstract class StatelessMongoFStorage implements StatelessFieldStorage, ConstructableValue, IMongoStorage {
+
+ protected final Class keyClass;
+ protected final Class valueClass;
+ protected final String idFieldName;
+ protected final MongoClient mongoClient;
+ protected final String uri;
+ @Getter
+ protected final MongoCollection collection;
+ protected final ReplaceOptions replaceOptions = new ReplaceOptions().upsert(true);
+
+ public StatelessMongoFStorage(final Class keyClass, final Class valueClass, final Credentials credentials) {
+ this(credentials.getUri(), credentials.getDatabase(), credentials.getCollection(), keyClass, valueClass);
+ }
+
+ public StatelessMongoFStorage(final String uri, final String database, final String collection, final Class keyClass, final Class valueClass) {
+ this.valueClass = valueClass;
+ this.keyClass = keyClass;
+ this.idFieldName = IdUtils.getIdName(this.valueClass);
+ try {
+ mongoClient = MongoProvider.getClient(uri);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to connect to mongo");
+ }
+ this.uri = uri;
+
+ MongoDatabase mongoDatabase = mongoClient.getDatabase(database);
+ this.collection = mongoDatabase.getCollection(collection);
+ }
+
+ @Override
+ public Class key() {
+ return keyClass;
+ }
+
+ @Override
+ public Class value() {
+ return valueClass;
+ }
+
+ @Override
+ public CompletableFuture> get(int limit, Filter... filters) {
+ return CompletableFuture.supplyAsync(() -> {
+ boolean hasLimit = limit > 0;
+ List> group = Filter.group(filters);
+ boolean hasOr = group.size() > 1;
+
+ Document query = new Document();
+ List orConditions = new ArrayList<>();
+
+ for (List filterGroup : group) {
+ Document andQuery = andFilters(filterGroup.toArray(new Filter[0]));
+
+ if (hasOr) {
+ orConditions.add(andQuery);
+ } else {
+ query = andQuery;
+ }
+ }
+
+ if (hasOr) {
+ query.append("$or", orConditions);
+ }
+
+ FindIterable iterable = collection.find(query);
+ if (hasLimit) {
+ iterable.limit(limit);
+ }
+
+ List values = new ArrayList<>();
+ for (Document document : iterable) {
+ values.add(StorageAPIConstants.getGson().fromJson(document.toJson(), valueClass));
+ }
+ return values;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture get(K key) {
+ return CompletableFuture.supplyAsync(() -> {
+ Document document = collection.find(new Document(idFieldName, IdUtils.getId(valueClass, key))).first();
+ if (document == null) {
+ return null;
+ }
+ return StorageAPIConstants.getGson().fromJson(document.toJson(), valueClass);
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture save(V value) {
+ return CompletableFuture.supplyAsync(() -> {
+ Document document = Document.parse(StorageAPIConstants.getGson().toJson(value));
+ collection.replaceOne(new Document(idFieldName, IdUtils.getId(valueClass, value)), document, replaceOptions);
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture saveAll(Collection values) {
+ return CompletableFuture.supplyAsync(() -> {
+ for (V value : values) {
+ Document document = Document.parse(StorageAPIConstants.getGson().toJson(value));
+ collection.replaceOne(new Document(idFieldName, IdUtils.getId(valueClass, value)), document, replaceOptions);
+ }
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture remove(V key) {
+ return CompletableFuture.supplyAsync(() -> {
+ collection.deleteOne(new Document(idFieldName, IdUtils.getId(valueClass, key)));
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture write() {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ @Override
+ public CompletableFuture deleteAll() {
+ return CompletableFuture.supplyAsync(() -> {
+ collection.deleteMany(new Document());
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture contains(String field, Object value) {
+ return CompletableFuture.supplyAsync(() -> collection.find(new Document(field, value)).first() != null, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture> allValues() {
+ return CompletableFuture.supplyAsync(() -> {
+ List values = new ArrayList<>();
+ FindIterable iterable = collection.find();
+ for (Document document : iterable) {
+ values.add(StorageAPIConstants.getGson().fromJson(document.toJson(), valueClass));
+ }
+ return values;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture addIndex(String field) {
+ return CompletableFuture.supplyAsync(() -> {
+ collection.createIndex(new Document(field, 1));
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ @Override
+ public CompletableFuture removeIndex(String field) {
+ return CompletableFuture.supplyAsync(() -> {
+ collection.dropIndex(field);
+ return null;
+ }, StorageAPIConstants.DB_THREAD_POOL);
+ }
+
+ private Document andFilters(Filter... filters) {
+ Document query = new Document();
+ for (Filter filter : filters) {
+ query.putAll(filterToDocument(filter));
+ }
+ return query;
+ }
+
+ private Document filterToDocument(Filter filter) {
+ switch (filter.filterType()) {
+ case STARTS_WITH -> {
+ return new Document(filter.key(), new Document("$regex", "^" + filter.value()).append("$options", "i"));
+ }
+ case LESS_THAN, NOT_GREATER_THAN_OR_EQUAL_TO -> {
+ return new Document(filter.key(), new Document("$lt", filter.value()));
+ }
+ case EQUALS -> {
+ return new Document(filter.key(), filter.value());
+ }
+ case GREATER_THAN, NOT_LESS_THAN_OR_EQUAL_TO -> {
+ return new Document(filter.key(), new Document("$gt", filter.value()));
+ }
+ case CONTAINS -> {
+ return new Document(filter.key(), new Document("$regex", filter.value()).append("$options", "i"));
+ }
+ case ENDS_WITH -> {
+ return new Document(filter.key(), new Document("$regex", filter.value() + "$").append("$options", "i"));
+ }
+ case LESS_THAN_OR_EQUAL_TO, NOT_GREATER_THAN -> {
+ return new Document(filter.key(), new Document("$lte", filter.value()));
+ }
+ case GREATER_THAN_OR_EQUAL_TO, NOT_LESS_THAN -> {
+ return new Document(filter.key(), new Document("$gte", filter.value()));
+ }
+ case NOT_EQUALS -> {
+ return new Document(filter.key(), new Document("$ne", filter.value()));
+ }
+ case NOT_CONTAINS -> {
+ return new Document(filter.key(), new Document("$not", new Document("$regex", filter.value()).append("$options", "i")));
+ }
+ case NOT_STARTS_WITH -> {
+ return new Document(filter.key(), new Document("$not", new Document("$regex", "^" + filter.value()).append("$options", "i")));
+ }
+ case NOT_ENDS_WITH -> {
+ return new Document(filter.key(), new Document("$not", new Document("$regex", filter.value() + "$").append("$options", "i")));
+ }
+ default -> {
+ throw new IllegalArgumentException("Unknown filter type: " + filter.filterType());
+ }
+ }
+ }
+}
diff --git a/src/test/java/wtf/casper/storageapi/FStorageTests.java b/src/test/java/wtf/casper/storageapi/FStorageTests.java
deleted file mode 100644
index 709cb88..0000000
--- a/src/test/java/wtf/casper/storageapi/FStorageTests.java
+++ /dev/null
@@ -1,268 +0,0 @@
-package wtf.casper.storageapi;
-
-import lombok.extern.java.Log;
-//import org.junit.jupiter.api.BeforeAll;
-//import org.junit.jupiter.api.Test;
-//import wtf.casper.storageapi.impl.direct.fstorage.*;
-//
-//import java.io.File;
-//import java.io.InputStream;
-//import java.util.Collection;
-//import java.util.List;
-//import java.util.Properties;
-//import java.util.UUID;
-//import java.util.concurrent.CompletableFuture;
-//
-//import static org.junit.Assert.assertEquals;
-
-@Log
-public class FStorageTests {
-
-// @BeforeAll
-// public static void setup() {
-// InputStream stream = FStorageTests.class.getClassLoader().getResourceAsStream("storage.properties");
-// File file = new File("."+File.separator+"storage.properties");
-// if (file.exists()) {
-// stream = file.toURI().toASCIIString().contains("jar") ? FStorageTests.class.getClassLoader().getResourceAsStream("storage.properties") : null;
-// }
-//
-// if (stream == null) {
-// log.severe("Could not find storage.properties file!");
-// return;
-// }
-//
-// Properties properties = new Properties();
-// try {
-// properties.load(stream);
-// } catch (Exception e) {
-// e.printStackTrace();
-// }
-//
-// init(properties);
-// }
-//
-// public static void init(Properties properties) {
-// StorageType type = StorageType.valueOf((String) properties.get("storage.type"));
-// credentials = Credentials.of(
-// type,
-// (String) properties.get("storage.host"),
-// (String) properties.get("storage.username"),
-// (String) properties.get("storage.password"),
-// (String) properties.get("storage.database"),
-// (String) properties.get("storage.collection"),
-// (String) properties.get("storage.table"),
-// (String) properties.get("storage.uri"),
-// Integer.parseInt((String) properties.get("storage.port"))
-// );
-//
-//
-// switch (type) {
-// case MONGODB -> storage = new DirectMongoFStorage<>(UUID.class, TestObject.class, credentials, TestObject::new);
-// case SQLITE -> throw new UnsupportedOperationException("SQLite is not supported yet!");
-// case SQL -> throw new UnsupportedOperationException("SQL is not supported yet!");
-// case MARIADB -> throw new UnsupportedOperationException("MariaDB is not supported yet!");
-// case JSON -> storage = new DirectJsonFStorage<>(UUID.class, TestObject.class, new File("."+File.separator+"data.json"), TestObject::new);
-// default -> throw new IllegalStateException("Unexpected value: " + type);
-// }
-//
-// storage.deleteAll().join();
-// storage.saveAll(initialData).join();
-// storage.write().join();
-// }
-//
-// private static Credentials credentials;
-// private static FieldStorage storage;
-//
-// private static final List initialData = List.of(
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000002"), "Mike", 25,
-// new TestObjectData("5678 Elm Avenue", "Fake Employer C", "fakemikec@gmail.com", "987-654-3210",
-// 15, new TestObjectBalance(150, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000003"), "Emily", 22,
-// new TestObjectData("7890 Oak Street", "Fake Employer D", "fakeemilyd@gmail.com", "555-555-5555",
-// 17, new TestObjectBalance(50, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000004"), "Michael", 30,
-// new TestObjectData("1111 Maple Avenue", "Fake Employer E", "fakemichaele@gmail.com", "111-222-3333",
-// 19, new TestObjectBalance(300, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000005"), "Sarah", 27,
-// new TestObjectData("2222 Pine Street", "Fake Employer F", "fakesarahf@gmail.com", "444-555-6666",
-// 18, new TestObjectBalance(75, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000006"), "David", 32,
-// new TestObjectData("3333 Cedar Avenue", "Fake Employer G", "fakedavidg@gmail.com", "777-888-9999",
-// 20, new TestObjectBalance(250, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000007"), "Olivia", 21,
-// new TestObjectData("4444 Birch Street", "Fake Employer H", "fakeoliviah@gmail.com", "000-111-2222",
-// 21, new TestObjectBalance(125, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000008"), "Daniel", 29,
-// new TestObjectData("5555 Willow Avenue", "Fake Employer I", "fakedanieli@gmail.com", "333-444-5555",
-// 18, new TestObjectBalance(180, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000009"), "Sophia", 26,
-// new TestObjectData("6666 Elm Avenue", "Fake Employer J", "fakesophiaj@gmail.com", "666-777-8888",
-// 16, new TestObjectBalance(90, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000010"), "James", 28,
-// new TestObjectData("7777 Oak Street", "Fake Employer K", "fakejamesk@gmail.com", "999-000-1111",
-// 18, new TestObjectBalance(160, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000011"), "Emma", 23,
-// new TestObjectData("8888 Maple Avenue", "Fake Employer L", "fakeemmal@gmail.com", "222-333-4444",
-// 10, new TestObjectBalance(220, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000012"), "Benjamin", 31,
-// new TestObjectData("9999 Pine Street", "Fake Employer M", "fakebenjaminm@gmail.com", "555-666-7777",
-// 55, new TestObjectBalance(110, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000013"), "Ava", 24,
-// new TestObjectData("1111 Cedar Avenue", "Fake Employer N", "fakeavan@gmail.com", "888-999-0000",
-// 66666, new TestObjectBalance(270, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000014"), "Ethan", 33,
-// new TestObjectData("2222 Birch Street", "Fake Employer O", "fakeethano@gmail.com", "111-222-3333",
-// 888, new TestObjectBalance(80, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000015"), "Mia", 20,
-// new TestObjectData("3333 Willow Avenue", "Fake Employer P", "fakemiap@gmail.com", "444-555-6666",
-// 0, new TestObjectBalance(140, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000000"), "John", 18,
-// new TestObjectData("1234 Fake Street", "Fake Employer A", "fakejohna@gmail.com", "123-456-7890",
-// 11, new TestObjectBalance(100, "USD")
-// )
-// ),
-// new TestObject(
-// UUID.fromString("00000000-0000-0000-0000-000000000001"), "Jane", 19,
-// new TestObjectData("1234 Fake Street", "Fake Employer B", "fakejanea@gmail.com", "123-456-7890",
-// 19, new TestObjectBalance(200, "USD")
-// )
-// )
-// );
-//
-//
-// @Test
-// public void testTotalData() {
-// log.fine(" --- Testing total data...");
-// assertEquals(initialData.size(), storage.allValues().join().size());
-// log.fine(" --- Total data test passed!");
-// }
-//
-// @Test
-// public void testStartsWith() {
-// log.fine(" --- Testing filter starts with...");
-//
-// Collection street = storage.get(
-// Filter.of("data.address", "1", FilterType.STARTS_WITH)
-// ).join();
-// assertEquals(4, street.size());
-//
-// log.fine(" --- Filter starts with test passed!");
-// }
-//
-// @Test
-// public void testEndsWith() {
-// log.fine(" --- Testing filter ends with...");
-//
-// Collection street = storage.get(
-// Filter.of("name", "a", FilterType.ENDS_WITH)
-// ).join();
-// assertEquals(5, street.size());
-//
-// Collection phone = storage.get(
-// Filter.of("data.phone", "0", FilterType.ENDS_WITH)
-// ).join();
-// assertEquals(4, phone.size());
-//
-// log.fine(" --- Filter starts with test passed!");
-// }
-//
-// @Test
-// public void testGreaterThan() {
-// log.fine(" --- Testing filter greater than...");
-//
-// Collection street = storage.get(
-// Filter.of("age", 20, FilterType.GREATER_THAN)
-// ).join();
-// assertEquals(13, street.size());
-//
-// log.fine(" --- Filter greater than test passed!");
-// }
-//
-// @Test
-// public void testLessThan() {
-// log.fine(" --- Testing filter less than...");
-//
-// Collection street = storage.get(
-// Filter.of("age", 20, FilterType.LESS_THAN)
-// ).join();
-// assertEquals(2, street.size());
-//
-// log.fine(" --- Filter less than test passed!");
-// }
-//
-// @Test
-// public void testContains() {
-// log.fine(" --- Testing filter contains...");
-//
-// Collection street = storage.get(
-// Filter.of("data.address", "Street", FilterType.CONTAINS),
-// Filter.of("age", 18, FilterType.EQUALS)
-// ).join();
-// assertEquals(1, street.size());
-//
-// Collection street1 = storage.get(
-// Filter.of("data.address", "Street", FilterType.CONTAINS)
-// ).join();
-// assertEquals(8, street1.size());
-//
-// CompletableFuture> allStreets = storage.get(
-// Filter.of("data.address", "Street", FilterType.CONTAINS),
-// Filter.of("data.address", "Avenue", FilterType.CONTAINS, SortingType.NONE, Filter.Type.OR)
-// );
-// assertEquals(16, allStreets.join().size());
-// log.fine(" --- Filter contains test passed!");
-// }
-//
-// @Test
-// public void testEquals() {
-// log.fine(" --- Testing filter equals...");
-//
-// Collection usd = storage.get(
-// Filter.of("data.balance.currency", "USD", FilterType.EQUALS)
-// ).join();
-// assertEquals(16, usd.size());
-// }
-}
diff --git a/src/test/java/wtf/casper/storageapi/KVStorageTests.java b/src/test/java/wtf/casper/storageapi/KVStorageTests.java
index 1aa4682..1c99778 100644
--- a/src/test/java/wtf/casper/storageapi/KVStorageTests.java
+++ b/src/test/java/wtf/casper/storageapi/KVStorageTests.java
@@ -15,7 +15,7 @@
import static org.junit.Assert.assertEquals;
@Log
-//TODO: move to https://java.testcontainers.org/
+//TODO: make stateless version of tests
public class KVStorageTests {
@BeforeAll
diff --git a/src/test/java/wtf/casper/storageapi/StatelessFStorageTests.java b/src/test/java/wtf/casper/storageapi/StatelessFStorageTests.java
new file mode 100644
index 0000000..19be065
--- /dev/null
+++ b/src/test/java/wtf/casper/storageapi/StatelessFStorageTests.java
@@ -0,0 +1,338 @@
+package wtf.casper.storageapi;
+
+import lombok.extern.java.Log;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import wtf.casper.storageapi.impl.direct.statelessfstorage.DirectStatelessMariaDBFStorage;
+import wtf.casper.storageapi.impl.direct.statelessfstorage.DirectStatelessMongoFStorage;
+
+@Log
+public class StatelessFStorageTests {
+
+ @BeforeAll
+ public static void setup() {
+ InputStream stream = StatelessFStorageTests.class.getClassLoader().getResourceAsStream("storage.properties");
+ File file = new File("."+File.separator+"storage.properties");
+ if (file.exists()) {
+ stream = file.toURI().toASCIIString().contains("jar") ? StatelessFStorageTests.class.getClassLoader().getResourceAsStream("storage.properties") : null;
+ }
+
+ if (stream == null) {
+ log.severe("Could not find storage.properties file!");
+ return;
+ }
+
+ Properties properties = new Properties();
+ try {
+ properties.load(stream);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ init(properties);
+ }
+
+ public static void init(Properties properties) {
+ StorageType type = StorageType.valueOf((String) properties.get("storage.type"));
+ credentials = Credentials.of(
+ type,
+ (String) properties.get("storage.host"),
+ (String) properties.get("storage.username"),
+ (String) properties.get("storage.password"),
+ (String) properties.get("storage.database"),
+ (String) properties.get("storage.collection"),
+ (String) properties.get("storage.table"),
+ (String) properties.get("storage.uri"),
+ Integer.parseInt((String) properties.get("storage.port"))
+ );
+
+
+ switch (type) {
+ case MONGODB -> storage = new DirectStatelessMongoFStorage<>(UUID.class, TestObject.class, credentials, TestObject::new);
+ case SQLITE -> throw new UnsupportedOperationException("SQLite is not supported yet!");
+ case SQL -> throw new UnsupportedOperationException("SQL is not supported yet!");
+ case MARIADB -> storage = new DirectStatelessMariaDBFStorage<>(UUID.class, TestObject.class, credentials, TestObject::new);
+ case JSON -> throw new UnsupportedOperationException("JSON is not supported yet!");
+ default -> throw new IllegalStateException("Unexpected value: " + type);
+ }
+
+ storage.deleteAll().join();
+ storage.saveAll(initialData).join();
+ storage.write().join();
+ }
+
+ private static Credentials credentials;
+ private static StatelessFieldStorage storage;
+
+ private static final List initialData = List.of(
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000002"), "Mike", 25,
+ new TestObjectData("5678 Elm Avenue", "Fake Employer C", "fakemikec@gmail.com", "987-654-3210",
+ 15, new TestObjectBalance(150, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000003"), "Emily", 22,
+ new TestObjectData("7890 Oak Street", "Fake Employer D", "fakeemilyd@gmail.com", "555-555-5555",
+ 17, new TestObjectBalance(50, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000004"), "Michael", 30,
+ new TestObjectData("1111 Maple Avenue", "Fake Employer E", "fakemichaele@gmail.com", "111-222-3333",
+ 19, new TestObjectBalance(300, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000005"), "Sarah", 27,
+ new TestObjectData("2222 Pine Street", "Fake Employer F", "fakesarahf@gmail.com", "444-555-6666",
+ 18, new TestObjectBalance(75, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000006"), "David", 32,
+ new TestObjectData("3333 Cedar Avenue", "Fake Employer G", "fakedavidg@gmail.com", "777-888-9999",
+ 20, new TestObjectBalance(250, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000007"), "Olivia", 21,
+ new TestObjectData("4444 Birch Street", "Fake Employer H", "fakeoliviah@gmail.com", "000-111-2222",
+ 21, new TestObjectBalance(125, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000008"), "Daniel", 29,
+ new TestObjectData("5555 Willow Avenue", "Fake Employer I", "fakedanieli@gmail.com", "333-444-5555",
+ 18, new TestObjectBalance(180, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000009"), "Sophia", 26,
+ new TestObjectData("6666 Elm Avenue", "Fake Employer J", "fakesophiaj@gmail.com", "666-777-8888",
+ 16, new TestObjectBalance(90, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000010"), "James", 28,
+ new TestObjectData("7777 Oak Street", "Fake Employer K", "fakejamesk@gmail.com", "999-000-1111",
+ 18, new TestObjectBalance(160, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000011"), "Emma", 23,
+ new TestObjectData("8888 Maple Avenue", "Fake Employer L", "fakeemmal@gmail.com", "222-333-4444",
+ 10, new TestObjectBalance(220, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000012"), "Benjamin", 31,
+ new TestObjectData("9999 Pine Street", "Fake Employer M", "fakebenjaminm@gmail.com", "555-666-7777",
+ 55, new TestObjectBalance(110, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000013"), "Ava", 24,
+ new TestObjectData("1111 Cedar Avenue", "Fake Employer N", "fakeavan@gmail.com", "888-999-0000",
+ 66666, new TestObjectBalance(270, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000014"), "Ethan", 33,
+ new TestObjectData("2222 Birch Street", "Fake Employer O", "fakeethano@gmail.com", "111-222-3333",
+ 888, new TestObjectBalance(80, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000015"), "Mia", 20,
+ new TestObjectData("3333 Willow Avenue", "Fake Employer P", "fakemiap@gmail.com", "444-555-6666",
+ 0, new TestObjectBalance(140, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000000"), "John", 18,
+ new TestObjectData("1234 Fake Street", "Fake Employer A", "fakejohna@gmail.com", "123-456-7890",
+ 11, new TestObjectBalance(100, "USD")
+ )
+ ),
+ new TestObject(
+ UUID.fromString("00000000-0000-0000-0000-000000000001"), "Jane", 19,
+ new TestObjectData("1234 Fake Street", "Fake Employer B", "fakejanea@gmail.com", "123-456-7890",
+ 19, new TestObjectBalance(200, "GBP")
+ )
+ )
+ );
+
+
+ @Test
+ public void testTotalData() {
+ assertEquals(initialData.size(), storage.allValues().join().size());
+ }
+
+ @Test
+ public void testStartsWith() {
+ Collection street = storage.get(
+ Filter.of("data.address", "1", FilterType.STARTS_WITH)
+ ).join();
+ assertEquals(4, street.size());
+ }
+
+ @Test
+ public void testEndsWith() {
+
+ Collection street = storage.get(
+ Filter.of("name", "a", FilterType.ENDS_WITH)
+ ).join();
+ assertEquals(5, street.size());
+
+ Collection phone = storage.get(
+ Filter.of("data.phone", "0", FilterType.ENDS_WITH)
+ ).join();
+ assertEquals(4, phone.size());
+ }
+
+ @Test
+ public void testGreaterThan() {
+ Collection street = storage.get(
+ Filter.of("age", 20, FilterType.GREATER_THAN)
+ ).join();
+ assertEquals(13, street.size());
+ }
+
+ @Test
+ public void testLessThan() {
+ Collection street = storage.get(
+ Filter.of("age", 20, FilterType.LESS_THAN)
+ ).join();
+ assertEquals(2, street.size());
+ }
+
+ @Test
+ public void testContains() {
+ Collection street = storage.get(
+ Filter.of("data.address", "Street", FilterType.CONTAINS),
+ Filter.of("age", 18, FilterType.EQUALS)
+ ).join();
+ assertEquals(1, street.size());
+
+ Collection street1 = storage.get(
+ Filter.of("data.address", "Street", FilterType.CONTAINS)
+ ).join();
+ assertEquals(8, street1.size());
+
+ CompletableFuture> allStreets = storage.get(
+ Filter.of("data.address", "Street", FilterType.CONTAINS),
+ Filter.of("data.address", "Avenue", FilterType.CONTAINS, SortingType.NONE, Filter.Type.OR),
+ Filter.of("data.address", "Random Garbage", FilterType.CONTAINS, SortingType.NONE, Filter.Type.OR)
+
+ );
+ assertEquals(16, allStreets.join().size());
+ }
+
+ @Test
+ public void testEquals() {
+ Collection usd = storage.get(
+ Filter.of("data.balance.currency", "USD", FilterType.EQUALS)
+ ).join();
+ assertEquals(15, usd.size());
+ }
+
+ @Test
+ public void testNotEquals() {
+ Collection usd = storage.get(
+ Filter.of("data.balance.currency", "USD", FilterType.NOT_EQUALS)
+ ).join();
+ assertEquals(1, usd.size());
+ }
+
+ @Test
+ public void testNotContains() {
+ Collection street = storage.get(
+ Filter.of("data.address", "Street", FilterType.NOT_CONTAINS)
+ ).join();
+ assertEquals(8, street.size());
+ }
+
+ @Test
+ public void testNotStartsWith() {
+ Collection street = storage.get(
+ Filter.of("data.address", "1", FilterType.NOT_STARTS_WITH)
+ ).join();
+ assertEquals(12, street.size());
+ }
+
+ @Test
+ public void testNotEndsWith() {
+ Collection street = storage.get(
+ Filter.of("name", "a", FilterType.NOT_ENDS_WITH)
+ ).join();
+ assertEquals(11, street.size());
+ }
+
+ @Test
+ public void testLessThanOrEqualTo() {
+ Collection street = storage.get(
+ Filter.of("age", 20, FilterType.LESS_THAN_OR_EQUAL_TO)
+ ).join();
+ assertEquals(3, street.size());
+ }
+
+ @Test
+ public void testGreaterThanOrEqualTo() {
+ Collection street = storage.get(
+ Filter.of("age", 20, FilterType.GREATER_THAN_OR_EQUAL_TO)
+ ).join();
+ assertEquals(14, street.size());
+ }
+
+ @Test
+ public void testNotLessThan() {
+ Collection street = storage.get(
+ Filter.of("age", 20, FilterType.NOT_LESS_THAN)
+ ).join();
+ assertEquals(14, street.size());
+ }
+
+ @Test
+ public void testNotGreaterThan() {
+ Collection street = storage.get(
+ Filter.of("age", 20, FilterType.NOT_GREATER_THAN)
+ ).join();
+ assertEquals(3, street.size());
+ }
+
+ @Test
+ public void testNotLessThanOrEqualTo() {
+ Collection street = storage.get(
+ Filter.of("age", 20, FilterType.NOT_LESS_THAN_OR_EQUAL_TO)
+ ).join();
+ assertEquals(13, street.size());
+ }
+
+ @Test
+ public void testNotGreaterThanOrEqualTo() {
+ Collection street = storage.get(
+ Filter.of("age", 20, FilterType.NOT_GREATER_THAN_OR_EQUAL_TO)
+ ).join();
+ assertEquals(2, street.size());
+ }
+
+ @Test
+ public void testAnd() {
+ Collection street = storage.get(
+ Filter.of("age", 20, FilterType.GREATER_THAN),
+ Filter.of("data.address", "Street", FilterType.CONTAINS)
+ ).join();
+ assertEquals(6, street.size());
+ }
+
+ @Test
+ public void testLimit() {
+ Collection street = storage.get(10,
+ Filter.of("age", 20, FilterType.GREATER_THAN_OR_EQUAL_TO)
+ ).join();
+ assertEquals(10, street.size());
+ }
+
+
+}
diff --git a/src/test/resources/storage.properties b/src/test/resources/storage.properties
index abf51c2..1bd9cf8 100644
--- a/src/test/resources/storage.properties
+++ b/src/test/resources/storage.properties
@@ -1,4 +1,4 @@
-storage.type=JSON
+storage.type=MARIADB
storage.host=127.0.0.1
storage.username=root
storage.password=1212