From af7c36c65afb0ed13b0aec8048f60d8886107bf5 Mon Sep 17 00:00:00 2001
From: 0xb00bface <0xboobface@gmail.com>
Date: Sun, 19 Feb 2023 17:01:58 +0100
Subject: [PATCH] Use antlr4 for post-processing variable expansion
This will break the current syntax, but make it easier to extend functionality including the implementation of functions to convert data.
---
common/pom.xml | 30 +++++
.../variableexpansion/antlr/PostProcessing.g4 | 21 +++
common/src/main/java/ctbrec/StringUtil.java | 63 ++++++---
...AbstractPlaceholderAwarePostProcessor.java | 120 ++++++++----------
.../AbstractVariableExpander.java | 72 +++++------
.../ModelVariableExpander.java | 47 +++----
.../variableexpansion/ParserVisitor.java | 64 ++++++++++
.../variableexpansion/VarArgsFunction.java | 7 +
.../VariableExpansionException.java | 7 +
.../functions/AntlrSyntacErrorAdapter.java | 26 ++++
.../functions/Capitalize.java | 13 ++
.../variableexpansion/functions/Format.java | 27 ++++
.../variableexpansion/functions/Lower.java | 12 ++
.../variableexpansion/functions/Sanitize.java | 13 ++
.../variableexpansion/functions/Trim.java | 12 ++
.../variableexpansion/functions/Upper.java | 12 ++
...ractPlaceholderAwarePostProcessorTest.java | 21 ++-
17 files changed, 404 insertions(+), 163 deletions(-)
create mode 100644 common/src/main/antlr4/ctbrec/variableexpansion/antlr/PostProcessing.g4
create mode 100644 common/src/main/java/ctbrec/variableexpansion/ParserVisitor.java
create mode 100644 common/src/main/java/ctbrec/variableexpansion/VarArgsFunction.java
create mode 100644 common/src/main/java/ctbrec/variableexpansion/VariableExpansionException.java
create mode 100644 common/src/main/java/ctbrec/variableexpansion/functions/AntlrSyntacErrorAdapter.java
create mode 100644 common/src/main/java/ctbrec/variableexpansion/functions/Capitalize.java
create mode 100644 common/src/main/java/ctbrec/variableexpansion/functions/Format.java
create mode 100644 common/src/main/java/ctbrec/variableexpansion/functions/Lower.java
create mode 100644 common/src/main/java/ctbrec/variableexpansion/functions/Sanitize.java
create mode 100644 common/src/main/java/ctbrec/variableexpansion/functions/Trim.java
create mode 100644 common/src/main/java/ctbrec/variableexpansion/functions/Upper.java
diff --git a/common/pom.xml b/common/pom.xml
index a357a2d9..25c4a3fa 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -12,6 +12,10 @@
../master
+
+ 4.11.1
+
+
org.slf4j
@@ -62,6 +66,32 @@
2.3.1
runtime
+
+ org.antlr
+ antlr4-runtime
+ ${antlr.version}
+
+
+
+
+ org.antlr
+ antlr4-maven-plugin
+ ${antlr.version}
+
+
+
+ antlr4
+
+
+ true
+ true
+
+
+
+
+
+
+
diff --git a/common/src/main/antlr4/ctbrec/variableexpansion/antlr/PostProcessing.g4 b/common/src/main/antlr4/ctbrec/variableexpansion/antlr/PostProcessing.g4
new file mode 100644
index 00000000..ed95603b
--- /dev/null
+++ b/common/src/main/antlr4/ctbrec/variableexpansion/antlr/PostProcessing.g4
@@ -0,0 +1,21 @@
+grammar PostProcessing;
+
+line: (text | variable | functionCall)* EOF;
+
+functionCall: '$' identifier '(' (expression (',' expression)*)? ')';
+
+text: CH+?;
+identifier: CH+?;
+
+parameter: text;
+
+variable: '${' identifier ('(' parameter ')')? ('?' expression)? '}';
+
+expression
+ : text
+ | variable
+ | functionCall
+ ;
+
+CH: .;
+WS: [ \t\r\n]+ -> skip;
diff --git a/common/src/main/java/ctbrec/StringUtil.java b/common/src/main/java/ctbrec/StringUtil.java
index c1c93596..cdb20f44 100644
--- a/common/src/main/java/ctbrec/StringUtil.java
+++ b/common/src/main/java/ctbrec/StringUtil.java
@@ -8,7 +8,8 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StringUtil {
- private StringUtil() {}
+ private StringUtil() {
+ }
public static boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
@@ -55,8 +56,8 @@ public class StringUtil {
/**
* Converts one byte to its hex representation with leading zeros. E.g. 255 -> FF, 12 -> 0C
*
- * @param b
- * @return
+ * @param b the byte value to represent in hex
+ * @return the hexadecimal representation as string
*/
public static String toHexString(int b) {
String hex = Integer.toHexString(b & 0xFF);
@@ -122,20 +123,20 @@ public class StringUtil {
}
s = s.toLowerCase();
- s = s.replaceAll("-", " ");
- s = s.replaceAll(":", " ");
- s = s.replaceAll(";", " ");
- s = s.replaceAll("\\|", " ");
- s = s.replaceAll("_", " ");
- s = s.replaceAll("\\.", "\\. ");
+ s = s.replace("-", " ");
+ s = s.replace(":", " ");
+ s = s.replace(";", " ");
+ s = s.replace("\\|", " ");
+ s = s.replace("_", " ");
+ s = s.replace("\\.", "\\. ");
s = s.trim();
t = t.toLowerCase();
- t = t.replaceAll("-", " ");
- t = t.replaceAll(":", " ");
- t = t.replaceAll(";", " ");
- t = t.replaceAll("\\|", " ");
- t = t.replaceAll("_", " ");
- t = t.replaceAll("\\.", "\\. ");
+ t = t.replace("-", " ");
+ t = t.replace(":", " ");
+ t = t.replace(";", " ");
+ t = t.replace("\\|", " ");
+ t = t.replace("_", " ");
+ t = t.replace("\\.", "\\. ");
t = t.trim();
// calculate levenshteinDistance
@@ -150,7 +151,7 @@ public class StringUtil {
public static int getLevenshteinDistance(String s, String t) {
int n = s.length();
int m = t.length();
- int d[][] = new int[n + 1][m + 1];
+ int[][] d = new int[n + 1][m + 1];
int i;
int j;
int cost;
@@ -208,4 +209,34 @@ public class StringUtil {
}
return result.toArray(new String[0]);
}
+
+ public static String capitalize(String string) {
+ if (string.length() > 0) {
+ StringTokenizer st = new StringTokenizer(string, " _-.", true);
+ var sb = new StringBuilder();
+ while (st.hasMoreTokens()) {
+ replaceBlacklistedCharacters(sb, st.nextToken());
+ }
+ string = sb.toString();
+ }
+ return string;
+ }
+
+ private static void replaceBlacklistedCharacters(StringBuilder sb, String token) {
+ StringBuilder temp = new StringBuilder(token);
+ char first = temp.charAt(0);
+ if (first >= 'a' && first <= 'z') { // if first is a letter
+ first -= 32;
+ temp.setCharAt(0, first);
+ } else {
+ if (temp.length() > 1) {
+ char second = temp.charAt(1);
+ if (second >= 'a' && second <= 'z') {
+ second -= 32;
+ temp.setCharAt(1, second);
+ }
+ }
+ }
+ sb.append(temp);
+ }
}
diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java
index 238f900f..8ab1d590 100644
--- a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java
+++ b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java
@@ -4,98 +4,80 @@ import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.StringUtil;
import ctbrec.variableexpansion.ModelVariableExpander;
+import ctbrec.variableexpansion.ParserVisitor;
+import ctbrec.variableexpansion.VariableExpansionException;
+import ctbrec.variableexpansion.antlr.PostProcessingLexer;
+import ctbrec.variableexpansion.antlr.PostProcessingParser;
+import ctbrec.variableexpansion.functions.AntlrSyntacErrorAdapter;
+import lombok.extern.slf4j.Slf4j;
+import org.antlr.v4.runtime.*;
+import org.antlr.v4.runtime.tree.ParseTree;
+import java.io.IOException;
+import java.io.StringReader;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.*;
-import java.util.function.Function;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
import static java.util.Optional.ofNullable;
+@Slf4j
public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor {
public String fillInPlaceHolders(String input, PostProcessingContext ctx) {
Recording rec = ctx.getRecording();
Config config = ctx.getConfig();
-
ModelVariableExpander modelExpander = new ModelVariableExpander(rec.getModel(), config, ctx.getRecorder());
- Map>> placeholderValueSuppliers = new HashMap<>(modelExpander.getPlaceholderValueSuppliers());
- placeholderValueSuppliers.put("recordingNotes", r -> getSanitizedRecordingNotes(rec));
- placeholderValueSuppliers.put("fileSuffix", r -> getFileSuffix(rec));
- placeholderValueSuppliers.put("recordingsDir", r -> Optional.of(config.getSettings().recordingsDir));
- placeholderValueSuppliers.put("absolutePath", r -> Optional.of(rec.getPostProcessedFile().getAbsolutePath()));
- placeholderValueSuppliers.put("absoluteParentPath", r -> Optional.of(rec.getPostProcessedFile().getParentFile().getAbsolutePath()));
- placeholderValueSuppliers.put("epochSecond", r -> ofNullable(rec.getStartDate()).map(Instant::getEpochSecond).map(l -> Long.toString(l))); // NOSONAR
- placeholderValueSuppliers.put("utcDateTime", pattern -> replaceUtcDateTime(rec, pattern));
- placeholderValueSuppliers.put("localDateTime", pattern -> replaceLocalDateTime(rec, pattern));
+ Map> variables = new HashMap<>(modelExpander.getPlaceholderValueSuppliers());
+ variables.put("recordingNotes", getSanitizedRecordingNotes(rec));
+ variables.put("fileSuffix", getFileSuffix(rec));
+ variables.put("recordingsDir", Optional.of(config.getSettings().recordingsDir));
+ variables.put("absolutePath", Optional.of(rec.getPostProcessedFile().getAbsolutePath()));
+ variables.put("absoluteParentPath", Optional.of(rec.getPostProcessedFile().getParentFile().getAbsolutePath()));
+ variables.put("epochSecond", ofNullable(rec.getStartDate()).map(Instant::getEpochSecond).map(l -> Long.toString(l))); // NOSONAR
+ variables.put("utcDateTime", getUtcDateTime(rec));
+ variables.put("localDateTime", getLocalDateTime(rec));
- String output = fillInPlaceHolders(input, placeholderValueSuppliers);
- return output;
+ return fillInPlaceHolders(input, variables);
}
- private String fillInPlaceHolders(String input, Map>> placeholderValueSuppliers) {
- boolean somethingReplaced;
- do {
- somethingReplaced = false;
- int end = input.indexOf("}");
- if (end > 0) {
- int start = input.substring(0, end).lastIndexOf("${");
- if (start >= 0) {
- String placeholder = input.substring(start, end + 1);
- String placeholderName = placeholder.substring(2, placeholder.length() - 1);
- String defaultValue;
- String expression = null;
- int questionMark = placeholder.indexOf('?');
- if (questionMark > 0) {
- placeholderName = placeholder.substring(2, questionMark);
- defaultValue = placeholder.substring(questionMark + 1, placeholder.length() - 1);
- } else {
- defaultValue = "";
- }
- int bracket = placeholder.indexOf('(');
- if (bracket > 0) {
- placeholderName = placeholder.substring(2, bracket);
- expression = placeholder.substring(bracket + 1, placeholder.indexOf(')', bracket));
- }
-
- final String name = placeholderName;
- Optional optionalValue = placeholderValueSuppliers.getOrDefault(name, r -> Optional.of(name)).apply(expression);
- String value = optionalValue.orElse(defaultValue);
- StringBuilder sb = new StringBuilder(input);
- String output = sb.replace(start, end + 1, value).toString();
- somethingReplaced = !Objects.equals(input, output);
- input = output;
+ private String fillInPlaceHolders(String input, Map> variables) {
+ try (StringReader reader = new StringReader(input)) {
+ CharStream s = CharStreams.fromReader(reader);
+ PostProcessingLexer lexer = new PostProcessingLexer(s);
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ PostProcessingParser parser = new PostProcessingParser(tokens);
+ parser.addErrorListener(new AntlrSyntacErrorAdapter() {
+ @Override
+ public void syntaxError(Recognizer, ?> recognizer, Object o, int line, int pos, String s, RecognitionException e) {
+ log.warn("Syntax error at {}:{} {}", line, pos, s);
}
- }
- } while (somethingReplaced);
- return input;
+ });
+ ParseTree parseTree = parser.line();
+ ParserVisitor visitor = new ParserVisitor(variables);
+ return visitor.visit(parseTree);
+ } catch (IOException e) {
+ throw new VariableExpansionException("Couldn't replace placeholders", e);
+ }
}
- private Optional replaceUtcDateTime(Recording rec, String pattern) {
- return replaceDateTime(rec, pattern, ZoneOffset.UTC);
+ protected Optional