summaryrefslogtreecommitdiff
path: root/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java
diff options
context:
space:
mode:
Diffstat (limited to 'base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java')
-rw-r--r--base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java265
1 files changed, 265 insertions, 0 deletions
diff --git a/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java b/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java
new file mode 100644
index 0000000..7c5205b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java
@@ -0,0 +1,265 @@
+package bjc.utils.ioutils;
+
+import java.io.InputStream;
+import java.util.InputMismatchException;
+import java.util.Scanner;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.IPair;
+import bjc.utils.data.Identity;
+import bjc.utils.data.Pair;
+import bjc.utils.exceptions.UnknownPragmaException;
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.FunctionalStringTokenizer;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * This class parses a rules based config file, and uses it to drive a provided
+ * set of actions
+ *
+ * @author ben
+ *
+ * @param <E>
+ * The type of the state object to use
+ *
+ */
+public class RuleBasedConfigReader<E> {
+ /* Function to execute when starting a rule.
+ * Takes the tokenizer, and a pair of the read token and application state
+ */
+ private BiConsumer<FunctionalStringTokenizer, IPair<String, E>> start;
+
+ /*
+ * Function to use when continuing a rule
+ * Takes a tokenizer and application state
+ */
+ private BiConsumer<FunctionalStringTokenizer, E> continueRule;
+
+ /*
+ * Function to use when ending a rule
+ * Takes an application state
+ */
+ private Consumer<E> end;
+
+ /*
+ * Map of pragma names to pragma actions
+ * Pragma actions are functions taking a tokenizer and application state
+ */
+ private final IMap<String, BiConsumer<FunctionalStringTokenizer, E>> pragmas;
+
+ /**
+ * Create a new rule-based config reader
+ *
+ * @param start
+ * The action to fire when starting a rule
+ * @param continueRule
+ * The action to fire when continuing a rule
+ * @param end
+ * The action to fire when ending a rule
+ */
+ public RuleBasedConfigReader(final BiConsumer<FunctionalStringTokenizer, IPair<String, E>> start,
+ final BiConsumer<FunctionalStringTokenizer, E> continueRule, final Consumer<E> end) {
+ this.start = start;
+ this.continueRule = continueRule;
+ this.end = end;
+
+ this.pragmas = new FunctionalMap<>();
+ }
+
+ /**
+ * Add a pragma to this reader
+ *
+ * @param name
+ * The name of the pragma to add
+ * @param action
+ * The function to execute when this pragma is read
+ */
+ public void addPragma(final String name, final BiConsumer<FunctionalStringTokenizer, E> action) {
+ if (name == null) throw new NullPointerException("Pragma name must not be null");
+ else if (action == null) throw new NullPointerException("Pragma action must not be null");
+
+ pragmas.put(name, action);
+ }
+
+ private void continueRule(final E state, final boolean isRuleOpen, final String line) {
+ // Make sure our input is correct
+ if (isRuleOpen == false)
+ throw new InputMismatchException("Cannot continue rule with no rule open");
+ else if (continueRule == null)
+ throw new InputMismatchException("Rule continuation not supported for current grammar");
+
+ /*
+ * Accept the rule
+ */
+ continueRule.accept(new FunctionalStringTokenizer(line.substring(1), " "), state);
+ }
+
+ private boolean endRule(final E state, final boolean isRuleOpen) {
+ /*
+ * Ignore blank line without an open rule
+ */
+ if (isRuleOpen == false)
+ /*
+ * Do nothing
+ */
+ return false;
+ else {
+ /*
+ * Nothing happens on rule end
+ */
+ if (end != null) {
+ /*
+ * Process the rule ending
+ */
+ end.accept(state);
+ }
+
+ /*
+ * Return a closed rule
+ */
+ return false;
+ }
+ }
+
+ /**
+ * Run a stream through this reader
+ *
+ * @param input
+ * The stream to get input
+ * @param initialState
+ * The initial state of the reader
+ * @return The final state of the reader
+ */
+ public E fromStream(final InputStream input, final E initialState) {
+ if (input == null) throw new NullPointerException("Input stream must not be null");
+
+ /*
+ * Application state: We're giving this back later
+ */
+ final E state = initialState;
+
+ /*
+ * Prepare our input source
+ */
+ try (Scanner source = new Scanner(input)) {
+ source.useDelimiter("\n");
+ /*
+ * This is true when a rule's open
+ */
+ final IHolder<Boolean> isRuleOpen = new Identity<>(false);
+
+ /*
+ * Do something for every line of the file
+ */
+ source.forEachRemaining((line) -> {
+ /*
+ * Skip comment lines
+ */
+ if (line.startsWith("#") || line.startsWith("//"))
+ /*
+ * It's a comment
+ */
+ return;
+ else if (line.equals("")) {
+ /*
+ * End the rule
+ */
+ isRuleOpen.replace(endRule(state, isRuleOpen.getValue()));
+ } else if (line.startsWith("\t")) {
+ /*
+ * Continue the rule
+ */
+ continueRule(state, isRuleOpen.getValue(), line);
+ } else {
+ /*
+ * Open a rule
+ */
+ isRuleOpen.replace(startRule(state, isRuleOpen.getValue(), line));
+ }
+ });
+ }
+
+ /*
+ * Return the state that the user has created
+ */
+ return state;
+ }
+
+ /**
+ * Set the action to execute when continuing a rule
+ *
+ * @param continueRule
+ * The action to execute on continuation of a rule
+ */
+ public void setContinueRule(final BiConsumer<FunctionalStringTokenizer, E> continueRule) {
+ this.continueRule = continueRule;
+ }
+
+ /**
+ * Set the action to execute when ending a rule
+ *
+ * @param end
+ * The action to execute on ending of a rule
+ */
+ public void setEndRule(final Consumer<E> end) {
+ this.end = end;
+ }
+
+ /**
+ * Set the action to execute when starting a rule
+ *
+ * @param start
+ * The action to execute on starting of a rule
+ */
+ public void setStartRule(final BiConsumer<FunctionalStringTokenizer, IPair<String, E>> start) {
+ if (start == null) throw new NullPointerException("Action on rule start must be non-null");
+
+ this.start = start;
+ }
+
+ private boolean startRule(final E state, boolean isRuleOpen, final String line) {
+ /*
+ * Create the line tokenizer
+ */
+ final FunctionalStringTokenizer tokenizer = new FunctionalStringTokenizer(line, " ");
+
+ /*
+ * Get the initial token
+ */
+ final String nextToken = tokenizer.nextToken();
+
+ /*
+ * Handle pragmas
+ */
+ if (nextToken.equals("pragma")) {
+ /*
+ * Get the pragma name
+ */
+ final String token = tokenizer.nextToken();
+
+ /*
+ * Handle pragmas
+ */
+ pragmas.getOrDefault(token, (tokenzer, stat) -> {
+ throw new UnknownPragmaException("Unknown pragma " + token);
+ }).accept(tokenizer, state);
+ } else {
+ /*
+ * Make sure input is correct
+ */
+ if (isRuleOpen == true)
+ throw new InputMismatchException("Nested rules are currently not supported");
+
+ /*
+ * Start a rule
+ */
+ start.accept(tokenizer, new Pair<>(nextToken, state));
+
+ isRuleOpen = true;
+ }
+
+ return isRuleOpen;
+ }
+}