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