commit 3353b4deb7cba0fb79e3b7ed8113b97971ea25b4 Author: 00asdf <53291579+00asdf@users.noreply.github.com> Date: Wed Mar 29 03:15:42 2023 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f68d109 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +### IntelliJ IDEA ### +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/artifacts/Confidibus_jar.xml b/.idea/artifacts/Confidibus_jar.xml new file mode 100644 index 0000000..e0fc620 --- /dev/null +++ b/.idea/artifacts/Confidibus_jar.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/out/artifacts/Confidibus_jar + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..2651417 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2fe2f58 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..3cc77f8 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1680031761802 + + + + + + + + file://$PROJECT_DIR$/src/dev/asdf00/confidibus/Confidibus.java + 180 + + + file://$PROJECT_DIR$/src/dev/asdf00/confidibus/Confidibus.java + 252 + + + + + \ No newline at end of file diff --git a/Confidibus.iml b/Confidibus.iml new file mode 100644 index 0000000..c90834f --- /dev/null +++ b/Confidibus.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/dev/asdf00/confidibus/Confidibus.java b/src/dev/asdf00/confidibus/Confidibus.java new file mode 100644 index 0000000..a03e142 --- /dev/null +++ b/src/dev/asdf00/confidibus/Confidibus.java @@ -0,0 +1,279 @@ +package dev.asdf00.confidibus; + +import dev.asdf00.confidibus.annotations.Config; +import dev.asdf00.confidibus.annotations.Section; +import dev.asdf00.confidibus.annotations.Value; + +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +/** + * Helper class to initialize and read a configuration file. Call {@code init()} to parse the configuration file. + *
+ * The {@code configClass} needs to be annotated with @Config and @Section. It may contain inner classes annotated + * with @Section. Every value needs to be public, static and annotated with @Value. + *
+ * This config only allows primitive types and strings. + */ +public class Confidibus { + private final Class _class; + private final PrintStream _debug; + private StringBuilder sb = new StringBuilder(); + private int indent = 0; + + + private Confidibus(Class configClass, PrintStream debugMsgStream) { + _class = configClass; + _debug = debugMsgStream; + } + + + /** + * Creates helper instance. + * @param configClass class containing the annotations for a config + * @param debugMsgStream {@link PrintStream} where debug output shall be written to. + * Can be {@code null} if no debug output should be written + */ + public static Confidibus of(Class configClass, PrintStream debugMsgStream) { + return new Confidibus(configClass, debugMsgStream); + } + + /** + * Initialize values in {@code configClass}. + * @param createIfMissing permission to create new config file if none is found + */ + public void init(boolean createIfMissing) { + Config cAnn = _class.getAnnotation(Config.class); + if (cAnn == null) { + throw new ConfigurationException("missing @Config annotation for %s!", _class.getSimpleName()); + } + Section cSect = _class.getAnnotation(Section.class); + if (cSect == null) { + throw new ConfigurationException("missing @Section annotation for %s!", _class.getSimpleName()); + } + Path cPath = Path.of(cAnn.path()); + if (Files.exists(cPath)) { + printDebug("found config file at %s!", cPath.toString()); + try { + parseFile(Files.readAllLines(cPath).iterator(), parseConfigClass(_class), true); + } catch (IOException e) { + throw new ConfigurationException("error reading %s!", cPath.toString()); + } + } else if (createIfMissing) { + printDebug("creating new config file at %s!", cPath.toString()); + try { + Files.writeString(cPath, initNewSection(_class).toString(), StandardOpenOption.CREATE_NEW); + } catch (IOException e) { + throw new ConfigurationException(e, "error creating new config file!"); + } + } else { + throw new ConfigurationException("did not find %s and was not allowed to create a new file!", cPath.toString()); + } + } + + + private static boolean setConfigValue(Field f, String v) { + if (!isValidConfigValue(f)) { + return false; + } + try { + if (f.getType().equals(byte.class)) { + f.set(null, Byte.parseByte(v)); + } else if (f.getType().equals(short.class)) { + f.set(null, Short.parseShort(v)); + } else if (f.getType().equals(int.class)) { + f.set(null, Integer.parseInt(v)); + } else if (f.getType().equals(long.class)) { + f.set(null, Long.parseLong(v)); + } else if (f.getType().equals(float.class)) { + f.set(null, Float.parseFloat(v)); + } else if (f.getType().equals(double.class)) { + f.set(null, Double.parseDouble(v)); + } else if (f.getType().equals(boolean.class)) { + f.set(null, Boolean.parseBoolean(v)); + } else if (f.getType().equals(char.class)) { + f.set(null, v.charAt(0)); + } else if (f.getType().equals(String.class)) { + f.set(null, v); + } else { + return false; + } + return true; + } catch (IllegalAccessException | NumberFormatException | IndexOutOfBoundsException e) { + return false; + } + } + + private static boolean isValidConfigValue(Field f) { + return f.getAnnotation(Value.class) != null && f.getAnnotation(Value.class).name().indexOf('\n') == -1 && + f.getAnnotation(Value.class).name().indexOf(':') == -1 && Modifier.isStatic(f.getModifiers()) && + !Modifier.isFinal(f.getModifiers()) && Modifier.isPublic(f.getModifiers()) && + (f.getType().isPrimitive() || f.getType().equals(String.class)); + } + + private static boolean isValidSection(Class c) { + Section s = c.getAnnotation(Section.class); + return s != null && s.title().indexOf('\n') == -1 && s.title().indexOf('{') == -1; + } + + private StringBuilder initNewSection(Class section) { + Section ann = section.getAnnotation(Section.class); + if (!ann.comment().equals("")) { + for (String line : ann.comment().split("\n")) { + sb.append(" ".repeat(indent)).append("// ").append(line).append('\n'); + } + } + if (ann.title().indexOf('\n') != -1) { + throw new ConfigurationException("newline in title of section %s is not allowed!", section.getSimpleName()); + } + sb.append(" ".repeat(indent)).append("[SECTION] ").append(ann.title()).append(" {\n"); + printDebug("initialized section %s defined in class %s!", ann.title(), section.getSimpleName()); + indent++; + boolean firstField = true; + for (Field f : section.getDeclaredFields()) { + Value v = f.getAnnotation(Value.class); + if (v != null) { + if (!isValidConfigValue(f)) { + throw new ConfigurationException("%s.%s is not applicable as a config value!", section.getSimpleName(), f.getName()); + } + if (firstField) { + firstField = false; + } else { + sb.append('\n'); + } + if (!v.comment().equals("")) { + for (String line : v.comment().split("\n")) { + sb.append(" ".repeat(indent)).append("// ").append(line).append('\n'); + } + } + String name = v.name(); + if (name.isEmpty()) { + name = f.getName(); + } + String type = f.getType().equals(String.class) ? "String" : f.getType().getTypeName(); + sb.append(" ".repeat(indent)).append('[').append(type).append("] ").append(name).append(": ") + .append(v._default()).append('\n'); + setConfigValue(f, v._default()); + printDebug("initialized %s(%s) with %s!", f.getName(), name, v._default()); + } + } + for (Class subSection : section.getDeclaredClasses()) { + Section subAnn = subSection.getAnnotation(Section.class); + if (subAnn != null) { + sb.append("\n\n"); + initNewSection(subSection); + } + } + indent--; + sb.append(" ".repeat(indent)).append("}\n"); + return sb; + } + + private void parseFile(Iterator file, CClass config, boolean first) { + boolean isOrigin = first; + boolean seenEnd = false; + while (file.hasNext()) { + String line = file.next(); + int i; + for (i = 0; i < line.length(); i++) { + if (line.charAt(i) != ' ') { + break; + } + } + if (i < line.length()) { + line = line.substring(i); + } + if (line.startsWith("[SECTION] ")) { + if (first) { + first = false; + } else { + int idx = line.indexOf(" {"); + if (idx == -1) { + throw new ConfigurationException("error reading line '%s'!", line); + } + String key = line.substring(10, idx); + if (!config.subSections.containsKey(key)) { + throw new ConfigurationException("section '%s' not found in class %s!", key, config.associatedClass.getSimpleName()); + } + parseFile(file, config.subSections.get(key), false); + } + } else if (line.startsWith("[")) { + int idx0 = line.indexOf("] "); + if (idx0 == -1) { + throw new ConfigurationException("error on value '%s'!", line); + } + int idx1 = line.indexOf(": "); + if (idx1 == -1) { + throw new ConfigurationException("error on value '%s'!", line); + } + String name = line.substring(idx0 + 2, idx1); + if (!config.vals.containsKey(name)) { + throw new ConfigurationException("value '%s' not found in class!", name); + } + String val = line.substring(idx1 + 2); + if (!setConfigValue(config.vals.get(name), val)) { + throw new ConfigurationException("error parsing '%s' as %s!", val, config.vals.get(name).getType()); + } + config.writtenFields.add(name); + } else if (line.startsWith("}")) { + seenEnd = true; + break; + } + } + if (isOrigin && !seenEnd) { + throw new ConfigurationException("missing '}'!"); + } + for (String fname : config.vals.keySet()) { + if (!config.writtenFields.contains(fname)) { + throw new ConfigurationException("not all fields of %s were written!", config.associatedClass.getSimpleName()); + } + } + } + + private CClass parseConfigClass(Class curClass) { + CClass config = new CClass(curClass); + for (Field f : curClass.getDeclaredFields()) { + if (isValidConfigValue(f)) { + Value v = f.getAnnotation(Value.class); + String name = v.name(); + if (name.isEmpty()) { + name = f.getName(); + } + config.vals.put(name, f); + } + } + for (Class subClass : curClass.getDeclaredClasses()) { + if (isValidSection(subClass)) { + Section sect = subClass.getAnnotation(Section.class); + config.subSections.put(sect.title(), parseConfigClass(subClass)); + } + } + return config; + } + + private void printDebug(String format, Object... params) { + if (_debug == null) { + return; + } + _debug.printf("[Confidibus] " + format + "\n", params); + } + + private static class CClass { + public final Class associatedClass; + public final HashMap vals = new HashMap<>(); + public final HashMap subSections = new HashMap<>(); + public final HashSet writtenFields = new HashSet<>(); + + public CClass(Class associatedClass) { + this.associatedClass = associatedClass; + } + } +} diff --git a/src/dev/asdf00/confidibus/ConfigurationException.java b/src/dev/asdf00/confidibus/ConfigurationException.java new file mode 100644 index 0000000..667e372 --- /dev/null +++ b/src/dev/asdf00/confidibus/ConfigurationException.java @@ -0,0 +1,10 @@ +package dev.asdf00.confidibus; + +public class ConfigurationException extends RuntimeException { + public ConfigurationException(String format, Object... params) { + super(String.format(format, params)); + } + public ConfigurationException(Throwable cause, String format, Object... params) { + super(String.format(format, params), cause); + } +} diff --git a/src/dev/asdf00/confidibus/annotations/Config.java b/src/dev/asdf00/confidibus/annotations/Config.java new file mode 100644 index 0000000..550d022 --- /dev/null +++ b/src/dev/asdf00/confidibus/annotations/Config.java @@ -0,0 +1,15 @@ +package dev.asdf00.confidibus.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Config { + /** + * path of the configuration file + */ + String path(); +} diff --git a/src/dev/asdf00/confidibus/annotations/Section.java b/src/dev/asdf00/confidibus/annotations/Section.java new file mode 100644 index 0000000..aacae58 --- /dev/null +++ b/src/dev/asdf00/confidibus/annotations/Section.java @@ -0,0 +1,14 @@ +package dev.asdf00.confidibus.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Section { + String title(); + + String comment() default ""; +} diff --git a/src/dev/asdf00/confidibus/annotations/Value.java b/src/dev/asdf00/confidibus/annotations/Value.java new file mode 100644 index 0000000..848eb26 --- /dev/null +++ b/src/dev/asdf00/confidibus/annotations/Value.java @@ -0,0 +1,19 @@ +package dev.asdf00.confidibus.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Value { + String comment() default ""; + + /** + * name of the value in the configuration file, if not specified, the name of the field will be written to the file + */ + String name() default ""; + + String _default(); +}