6 Commits

Author SHA1 Message Date
Oliver
c0998aabdb jdb: Fix index not deleting 2025-11-20 14:46:16 +01:00
Oliver
09363fe010 jdb: Add basic document indexing 2025-11-20 14:38:23 +01:00
Oliver
6c1ff8a77a config: Add VERSION file and configure publishing to maven repo 2025-11-20 12:29:25 +01:00
Oliver
0d8665e5e5 config: Add ConfigJSON 2025-11-20 12:26:58 +01:00
Oliver
b2416a14b2 jdb: Add VERSION file 2025-11-20 11:20:34 +01:00
Oliver
ec842f7287 jdb: Add countDocuments method 2025-11-20 11:06:00 +01:00
9 changed files with 396 additions and 5 deletions

View File

@@ -1,6 +1,5 @@
fancylibVersion=37
fancysitulaVersion=0.0.13
jdbVersion=1.0.1
plugintestsVersion=1.0.0
org.gradle.parallel=false
org.gradle.caching=true

1
libraries/config/VERSION Normal file
View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -24,6 +24,38 @@ dependencies {
}
tasks {
publishing {
repositories {
maven {
name = "fancyinnovationsReleases"
url = uri("https://repo.fancyinnovations.com/releases")
credentials(PasswordCredentials::class)
authentication {
isAllowInsecureProtocol = true
create<BasicAuthentication>("basic")
}
}
maven {
name = "fancyinnovationsSnapshots"
url = uri("https://repo.fancyinnovations.com/snapshots")
credentials(PasswordCredentials::class)
authentication {
isAllowInsecureProtocol = true
create<BasicAuthentication>("basic")
}
}
}
publications {
create<MavenPublication>("maven") {
groupId = "de.oliver"
artifactId = "config"
version = getCFGVersion()
from(project.components["java"])
}
}
}
compileJava {
options.encoding = Charsets.UTF_8.name()
options.release.set(17) //TODO change to 21, once 1.19.4 support is dropped
@@ -49,3 +81,7 @@ tasks {
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(17)) //TODO change to 21, once 1.19.4 support is dropped
}
fun getCFGVersion(): String {
return file("VERSION").readText()
}

View File

@@ -0,0 +1,233 @@
package com.fancyinnovations.config;
import com.google.gson.*;
import de.oliver.fancyanalytics.logger.ExtendedFancyLogger;
import de.oliver.fancyanalytics.logger.properties.ThrowableProperty;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ConfigJSON {
private final ExtendedFancyLogger logger;
private final File configFile;
private final Map<String, ConfigField<?>> fields;
private final Map<String, Object> values;
private final Gson gson;
public ConfigJSON(ExtendedFancyLogger logger, String configFilePath) {
this.logger = logger;
this.configFile = new File(configFilePath);
this.fields = new ConcurrentHashMap<>();
this.values = new ConcurrentHashMap<>();
this.gson = new GsonBuilder()
.serializeNulls()
.setPrettyPrinting()
.create();
}
public void addField(ConfigField<?> field) {
fields.put(field.path(), field);
}
public Map<String, ConfigField<?>> getFields() {
return fields;
}
public <T> T get(String path) {
ConfigField<?> field = fields.get(path);
if (field == null) {
return null;
}
if (field.forceDefault()) {
return (T) field.defaultValue();
}
Object value = values.computeIfAbsent(path, k -> field.defaultValue());
return (T) field.type().cast(value);
}
public void reload() {
if (!configFile.exists()) {
try {
File parent = configFile.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
if (!configFile.createNewFile()) {
logger.error("Failed to create config file: " + configFile.getAbsolutePath());
return;
}
} catch (IOException e) {
logger.error("Error creating config file: " + configFile.getAbsolutePath(), ThrowableProperty.of(e));
return;
}
JsonObject root = new JsonObject();
for (ConfigField<?> field : fields.values()) {
setDefault(root, field);
}
saveJson(root);
return;
}
JsonObject root;
try (FileReader reader = new FileReader(configFile)) {
JsonElement parsed = JsonParser.parseReader(reader);
if (parsed == null || !parsed.isJsonObject()) {
root = new JsonObject();
} else {
root = parsed.getAsJsonObject();
}
} catch (Exception e) {
logger.error("Error reading config file: " + configFile.getAbsolutePath(), ThrowableProperty.of(e));
root = new JsonObject();
}
boolean dirty = false;
for (Map.Entry<String, ConfigField<?>> entry : fields.entrySet()) {
String path = entry.getKey();
ConfigField<?> field = entry.getValue();
if (field.forRemoval()) {
if (isSet(root, path)) {
logger.debug("Removing path '" + path + "' from config");
removePath(root, path);
dirty = true;
}
continue;
}
JsonElement elem = getElement(root, path);
if (elem != null && !elem.isJsonNull()) {
try {
Object deserialized = gson.fromJson(elem, (Type) field.type());
if (deserialized != null && field.type().isInstance(deserialized)) {
values.put(path, deserialized);
} else {
// Attempt numeric conversions: gson may deserialize numbers as Double
Object converted = tryConvertNumber(deserialized, field.type());
if (converted != null) {
values.put(path, converted);
} else {
logger.warn("Value for path '" + path + "' is not of type '" + field.type().getSimpleName() + "'");
setDefault(root, field);
dirty = true;
}
}
} catch (JsonSyntaxException | ClassCastException ex) {
logger.warn("Failed to parse value for path '" + path + "': " + ex.getMessage());
setDefault(root, field);
dirty = true;
}
} else {
logger.debug("Path '" + path + "' not found in config");
setDefault(root, field);
dirty = true;
}
}
if (dirty) {
saveJson(root);
}
}
private void setDefault(JsonObject root, ConfigField<?> field) {
logger.debug("Setting default value for path '" + field.path() + "': " + field.defaultValue());
JsonElement elem = gson.toJsonTree(field.defaultValue(), (Type) field.type());
setElement(root, field.path(), elem);
// JSON does not support inline comments; descriptions are not stored.
}
private void saveJson(JsonObject root) {
try (FileWriter writer = new FileWriter(configFile)) {
gson.toJson(root, writer);
} catch (IOException e) {
logger.error("Error saving config file: " + configFile.getAbsolutePath(), ThrowableProperty.of(e));
}
}
/**
* Utility: get JsonElement at dot-separated path, or null if absent
*/
private JsonElement getElement(JsonObject root, String path) {
String[] parts = path.split("\\.");
JsonElement current = root;
for (String p : parts) {
if (!current.isJsonObject()) return null;
JsonObject obj = current.getAsJsonObject();
if (!obj.has(p)) return null;
current = obj.get(p);
}
return current;
}
/**
* Utility: set JsonElement at dot-separated path, creating intermediate objects
*/
private void setElement(JsonObject root, String path, JsonElement value) {
String[] parts = path.split("\\.");
JsonObject current = root;
for (int i = 0; i < parts.length - 1; i++) {
String p = parts[i];
if (!current.has(p) || !current.get(p).isJsonObject()) {
JsonObject child = new JsonObject();
current.add(p, child);
current = child;
} else {
current = current.getAsJsonObject(p);
}
}
current.add(parts[parts.length - 1], value);
}
private boolean isSet(JsonObject root, String path) {
return getElement(root, path) != null;
}
/**
* Remove a path (dot-separated) from the JSON object
*/
private void removePath(JsonObject root, String path) {
String[] parts = path.split("\\.");
JsonObject current = root;
for (int i = 0; i < parts.length - 1; i++) {
String p = parts[i];
if (!current.has(p) || !current.get(p).isJsonObject()) {
return;
}
current = current.getAsJsonObject(p);
}
current.remove(parts[parts.length - 1]);
}
/**
* Try to convert numeric values (e.g., Double) to the requested numeric target type
*/
private Object tryConvertNumber(Object value, Class<?> target) {
if (!(value instanceof Number)) return null;
Number num = (Number) value;
if (target == Integer.class || target == int.class) {
return num.intValue();
} else if (target == Long.class || target == long.class) {
return num.longValue();
} else if (target == Double.class || target == double.class) {
return num.doubleValue();
} else if (target == Float.class || target == float.class) {
return num.floatValue();
} else if (target == Short.class || target == short.class) {
return num.shortValue();
} else if (target == Byte.class || target == byte.class) {
return num.byteValue();
}
return null;
}
}

1
libraries/jdb/VERSION Normal file
View File

@@ -0,0 +1 @@
1.0.4

View File

@@ -1,11 +1,11 @@
plugins {
id("java")
id("maven-publish")
id("com.github.johnrengelman.shadow")
id("com.gradleup.shadow")
}
group = "de.oliver"
version = findProperty("jdbVersion") as String
version = getJDBVersion()
description = "Library for storing JSON data locally"
java {
@@ -55,7 +55,7 @@ tasks {
create<MavenPublication>("maven") {
groupId = "de.oliver"
artifactId = "JDB"
version = findProperty("jdbVersion") as String
version = getJDBVersion()
from(project.components["java"])
}
}
@@ -85,3 +85,7 @@ tasks {
useJUnitPlatform()
}
}
fun getJDBVersion(): String {
return file("VERSION").readText()
}

View File

@@ -9,6 +9,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -16,7 +17,7 @@ import java.util.Map;
* The JDB class provides a simple JSON document-based storage system in a specified directory.
*/
public class JDB {
private final static Gson GSON = new GsonBuilder()
public final static Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
.create();
@@ -24,6 +25,7 @@ public class JDB {
private static final String FILE_EXTENSION = ".json";
private final @NotNull String basePath;
private final @NotNull File baseDirectory;
private final JIndex index;
/**
* Constructs a new JDB instance with the specified base path.
@@ -33,6 +35,8 @@ public class JDB {
public JDB(@NotNull String basePath) {
this.basePath = basePath;
this.baseDirectory = new File(basePath);
this.index = JIndex.load("jdb_index", basePath);
}
/**
@@ -47,6 +51,13 @@ public class JDB {
public <T> T get(@NotNull String path, @NotNull Class<T> clazz) throws IOException {
File documentFile = new File(baseDirectory, createFilePath(path));
if (!documentFile.exists()) {
// Check index for alternative path
if (index.indexMap().containsKey(path)) {
String indexPath = index.indexMap().get(path);
return get(indexPath, clazz);
}
return null;
}
BufferedReader bufferedReader = Files.newBufferedReader(documentFile.toPath());
@@ -95,6 +106,26 @@ public class JDB {
return documents;
}
/**
* Counts the number of documents in the specified directory path.
*
* @param path the relative directory path
* @return the number of documents in the directory
*/
public int countDocuments(@NotNull String path) {
File directory = new File(baseDirectory, path);
if (!directory.exists()) {
return 0;
}
File[] files = directory.listFiles();
if (files == null) {
return 0;
}
return files.length;
}
/**
* Saves the given value as a document at the specified path.
*
@@ -113,12 +144,40 @@ public class JDB {
Files.write(documentFile.toPath(), json.getBytes());
}
/**
* Saves the given value as a document at the specified path and indexes it under additional paths.
*/
public <T> void set(@NotNull String path, @NotNull T value, String... indexPaths) throws IOException {
set(path, value);
for (String indexPath : indexPaths) {
indexDocument(indexPath, path);
}
}
/**
* Indexes a document by mapping the original path to the index path.
*
* @param originalPath the original relative path (excluding .json extension) of the document
* @param indexPath the index relative path (excluding .json extension) to map to the original document
*/
public void indexDocument(@NotNull String originalPath, @NotNull String indexPath) {
index.indexMap().put(originalPath, indexPath);
index.save();
}
/**
* Deletes the document(s) at the specified path.
*
* @param path the relative path (excluding .json extension) of the document(s) to be deleted
*/
public void delete(@NotNull String path) {
for (Map.Entry<String, String> entry : new HashSet<>(index.indexMap().entrySet())) {
if (entry.getKey().equals(path) || entry.getValue().equals(path)) {
index.indexMap().remove(entry.getKey());
index.save();
}
}
File file = new File(baseDirectory, path);
if (file.isDirectory()) {
deleteDirectory(file);

View File

@@ -0,0 +1,41 @@
package de.oliver.jdb;
import com.google.gson.annotations.SerializedName;
import java.io.File;
import java.nio.file.Files;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public record JIndex(
String name,
@SerializedName("base_path") String basePath,
@SerializedName("index_map") Map<String, String> indexMap // key -> original path
) {
public static JIndex load(String name, String basePath) {
File indexFile = new File(basePath, name + ".json");
if (!indexFile.exists()) {
return new JIndex(name, basePath, new ConcurrentHashMap<>());
}
try (var reader = Files.newBufferedReader(indexFile.toPath())) {
return JDB.GSON.fromJson(reader, JIndex.class);
} catch (Exception e) {
e.printStackTrace();
return new JIndex(name, basePath, new ConcurrentHashMap<>());
}
}
public void save() {
File indexFile = new File(basePath, name + ".json");
String json = JDB.GSON.toJson(this);
try {
Files.write(indexFile.toPath(), json.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -110,6 +110,23 @@ public class JDBTest {
assertTrue(result.isEmpty());
}
@Test
public void testCountDocuments() throws IOException {
// Prepare
String basePath = "./test_files/";
JDB jdb = new JDB(basePath);
String path = "test_files";
jdb.set(path + "/obj1", "Test message 1");
jdb.set(path + "/obj2", "Test message 2");
jdb.set(path + "/obj3", "Test message 3");
// Act
int count = jdb.countDocuments(path);
// Assert
assertEquals(3, count);
}
@Test
public void testSetNewObject() throws IOException {
// Prepare