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 getUtcDateTime(Recording rec) { + return Optional.ofNullable(rec) + .map(Recording::getStartDate) + .map(i -> i.atZone(ZoneOffset.UTC)); } - private Optional replaceLocalDateTime(Recording rec, String filename) { - return replaceDateTime(rec, filename, ZoneId.systemDefault()); + protected Optional getLocalDateTime(Recording rec) { + return Optional.ofNullable(rec) + .map(Recording::getStartDate) + .map(i -> i.atZone(ZoneId.systemDefault())); } - private Optional replaceDateTime(Recording rec, String pattern, ZoneId zone) { - pattern = pattern != null ? pattern : "yyyy-MM-dd_HH-mm-ss"; - return getDateTime(rec, pattern, zone); - } - - private Optional getDateTime(Recording rec, String pattern, ZoneId zone) { - return Optional.ofNullable(rec.getStartDate()) // - .map(DateTimeFormatter.ofPattern(pattern) // - .withLocale(Locale.getDefault()) // - .withZone(zone) // - ::format); - } - - private Optional getFileSuffix(Recording rec) { + private Optional getFileSuffix(Recording rec) { if (rec.isSingleFile()) { String filename = rec.getPostProcessedFile().getName(); return Optional.of(filename.substring(filename.lastIndexOf('.') + 1)); @@ -104,7 +86,7 @@ public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPost } } - private Optional getSanitizedRecordingNotes(Recording rec) { + private Optional getSanitizedRecordingNotes(Recording rec) { Optional notes = ofNullable(rec.getNote()); return notes.map(StringUtil::sanitize); } diff --git a/common/src/main/java/ctbrec/variableexpansion/AbstractVariableExpander.java b/common/src/main/java/ctbrec/variableexpansion/AbstractVariableExpander.java index ae528d6a..169dfd55 100644 --- a/common/src/main/java/ctbrec/variableexpansion/AbstractVariableExpander.java +++ b/common/src/main/java/ctbrec/variableexpansion/AbstractVariableExpander.java @@ -1,59 +1,45 @@ package ctbrec.variableexpansion; +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 java.io.IOException; +import java.io.StringReader; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Optional; -import java.util.Set; -import java.util.function.Function; +@Slf4j abstract class AbstractVariableExpander { - protected Map>> placeholderValueSuppliers = new HashMap<>(); + protected Map> placeholderValueSuppliers = new HashMap<>(); - protected String fillInPlaceHolders(String input, Map>> placeholderValueSuppliers) { - boolean somethingReplaced = false; - 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 = null; - 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; + protected 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); + }); + PostProcessingParser.LineContext ctx = parser.line(); + ParserVisitor visitor = new ParserVisitor(variables); + return visitor.visit(ctx); + } catch (IOException e) { + log.error("Couldn't fill in placeholders", e); + } + return input; } - public Set getPlaceholderNames() { - return placeholderValueSuppliers.keySet(); - } - - public Map>> getPlaceholderValueSuppliers() { + public Map> getPlaceholderValueSuppliers() { return placeholderValueSuppliers; } } diff --git a/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java b/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java index 9a3aad87..1db6e774 100644 --- a/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java +++ b/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java @@ -1,11 +1,5 @@ package ctbrec.variableexpansion; -import static ctbrec.StringUtil.*; -import static java.util.Optional.*; - -import java.util.Optional; -import java.util.UUID; - import ctbrec.Config; import ctbrec.Model; import ctbrec.ModelGroup; @@ -13,25 +7,30 @@ import ctbrec.StringUtil; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; +import java.util.Optional; +import java.util.UUID; + +import static java.util.Optional.ofNullable; + public class ModelVariableExpander extends AbstractVariableExpander { public ModelVariableExpander(Model model, Config config, Recorder recorder) { Optional modelGroup = Optional.ofNullable(recorder).flatMap(r -> r.getModelGroup(model)); - placeholderValueSuppliers.put("modelName", r -> ofNullable(model.getName())); - placeholderValueSuppliers.put("modelDisplayName", r -> ofNullable(model.getDisplayName())); - placeholderValueSuppliers.put("modelSanitizedName", r -> getSanitizedName(model)); - placeholderValueSuppliers.put("modelNotes", r -> getSanitizedModelNotes(config, model)); - placeholderValueSuppliers.put("siteName", r -> ofNullable(model).map(Model::getSite).map(Site::getName)); - placeholderValueSuppliers.put("siteSanitizedName", r -> getSanitizedSiteName(model)); - placeholderValueSuppliers.put("modelGroupName", r -> modelGroup.map(ModelGroup::getName)); - placeholderValueSuppliers.put("modelGroupId", r -> modelGroup.map(ModelGroup::getId).map(UUID::toString)); + placeholderValueSuppliers.put("modelName", ofNullable(model.getName())); + placeholderValueSuppliers.put("modelDisplayName", ofNullable(model.getDisplayName())); + placeholderValueSuppliers.put("modelSanitizedName",getSanitizedName(model)); + placeholderValueSuppliers.put("modelNotes",getSanitizedModelNotes(config, model)); + placeholderValueSuppliers.put("siteName", ofNullable(model).map(Model::getSite).map(Site::getName)); + placeholderValueSuppliers.put("siteSanitizedName",getSanitizedSiteName(model)); + placeholderValueSuppliers.put("modelGroupName", modelGroup.map(ModelGroup::getName)); + placeholderValueSuppliers.put("modelGroupId", modelGroup.map(ModelGroup::getId).map(UUID::toString)); } public String expand(String input) { return fillInPlaceHolders(input, placeholderValueSuppliers); } - private Optional getSanitizedName(Model model) { + private Optional getSanitizedName(Model model) { String name = model.getSanitizedNamed(); if (StringUtil.isBlank(name)) { return Optional.empty(); @@ -40,21 +39,11 @@ public class ModelVariableExpander extends AbstractVariableExpander { } } - private Optional getSanitizedSiteName(Model model) { - Optional name = ofNullable(model).map(Model::getSite).map(Site::getName); - if (name.isPresent()) { - return Optional.of(sanitize(name.get())); - } else { - return Optional.empty(); - } + private Optional getSanitizedSiteName(Model model) { + return ofNullable(model).map(Model::getSite).map(Site::getName).map(StringUtil::sanitize); } - private Optional getSanitizedModelNotes(Config config, Model m) { - Optional notes = ofNullable(config.getModelNotes(m)); - if (notes.isPresent()) { - return Optional.of(sanitize(notes.get())); - } else { - return Optional.empty(); - } + private Optional getSanitizedModelNotes(Config config, Model m) { + return ofNullable(config.getModelNotes(m)).map(StringUtil::sanitize); } } diff --git a/common/src/main/java/ctbrec/variableexpansion/ParserVisitor.java b/common/src/main/java/ctbrec/variableexpansion/ParserVisitor.java new file mode 100644 index 00000000..9939e830 --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/ParserVisitor.java @@ -0,0 +1,64 @@ +package ctbrec.variableexpansion; + +import ctbrec.variableexpansion.antlr.PostProcessingBaseVisitor; +import ctbrec.variableexpansion.antlr.PostProcessingParser; +import ctbrec.variableexpansion.functions.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class ParserVisitor extends PostProcessingBaseVisitor { + + private final Map> functions = new HashMap<>(); + private final Map> variables; + + public ParserVisitor(Map> variables) { + this.variables = variables; + functions.put("trim", new Trim()); + functions.put("upper", new Upper()); + functions.put("lower", new Lower()); + functions.put("format", new Format()); + functions.put("capitalize", new Capitalize()); + functions.put("sanitize", new Sanitize()); + } + + @Override + public String visitFunctionCall(PostProcessingParser.FunctionCallContext ctx) { + String identifier = ctx.identifier().getText(); + VarArgsFunction function = functions.get(identifier); + if (function != null) { + List parameters = ctx.expression(); + List params = parameters.stream().map(this::visitExpression).toList(); + return function.apply(params.toArray(new Object[0])); + } else { + return ctx.getText(); + } + } + + @Override + public String visitVariable(PostProcessingParser.VariableContext ctx) { + String identifier = ctx.identifier().getText(); + Optional value = variables.get(identifier); + if (value != null && value.isPresent()) { // NOSONAR + return value.get().toString(); + } else { + if (ctx.expression() != null) { + return visitExpression(ctx.expression()); + } else { + return ""; + } + } + } + + @Override + public String visitText(PostProcessingParser.TextContext ctx) { + return ctx.getText(); + } + + @Override + protected String aggregateResult(String aggregate, String nextResult) { + return Optional.ofNullable(aggregate).orElse("") + Optional.ofNullable(nextResult).orElse(""); + } +} diff --git a/common/src/main/java/ctbrec/variableexpansion/VarArgsFunction.java b/common/src/main/java/ctbrec/variableexpansion/VarArgsFunction.java new file mode 100644 index 00000000..5139f86d --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/VarArgsFunction.java @@ -0,0 +1,7 @@ +package ctbrec.variableexpansion; + +@FunctionalInterface +public interface VarArgsFunction { + + O apply(I... params); +} diff --git a/common/src/main/java/ctbrec/variableexpansion/VariableExpansionException.java b/common/src/main/java/ctbrec/variableexpansion/VariableExpansionException.java new file mode 100644 index 00000000..75075c99 --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/VariableExpansionException.java @@ -0,0 +1,7 @@ +package ctbrec.variableexpansion; + +public class VariableExpansionException extends RuntimeException { + public VariableExpansionException(String s, Exception e) { + super(s, e); + } +} diff --git a/common/src/main/java/ctbrec/variableexpansion/functions/AntlrSyntacErrorAdapter.java b/common/src/main/java/ctbrec/variableexpansion/functions/AntlrSyntacErrorAdapter.java new file mode 100644 index 00000000..684650d7 --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/functions/AntlrSyntacErrorAdapter.java @@ -0,0 +1,26 @@ +package ctbrec.variableexpansion.functions; + +import org.antlr.v4.runtime.ANTLRErrorListener; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.atn.ATNConfigSet; +import org.antlr.v4.runtime.dfa.DFA; + +import java.util.BitSet; + +public abstract class AntlrSyntacErrorAdapter implements ANTLRErrorListener { + + @Override + public void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean b, BitSet bitSet, ATNConfigSet atnConfigSet) { + // not intereseted + } + + @Override + public void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitSet, ATNConfigSet atnConfigSet) { + // not intereseted + } + + @Override + public void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atnConfigSet) { + // not intereseted + } +} diff --git a/common/src/main/java/ctbrec/variableexpansion/functions/Capitalize.java b/common/src/main/java/ctbrec/variableexpansion/functions/Capitalize.java new file mode 100644 index 00000000..3b28f36b --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/functions/Capitalize.java @@ -0,0 +1,13 @@ +package ctbrec.variableexpansion.functions; + +import ctbrec.StringUtil; +import ctbrec.variableexpansion.VarArgsFunction; + +import java.util.Optional; + +public class Capitalize implements VarArgsFunction { + @Override + public String apply(Object[] params) { + return Optional.ofNullable(params).map(p -> p[0]).map(String.class::cast).map(StringUtil::capitalize).orElse(""); + } +} diff --git a/common/src/main/java/ctbrec/variableexpansion/functions/Format.java b/common/src/main/java/ctbrec/variableexpansion/functions/Format.java new file mode 100644 index 00000000..b0c6ae88 --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/functions/Format.java @@ -0,0 +1,27 @@ +package ctbrec.variableexpansion.functions; + +import ctbrec.variableexpansion.VarArgsFunction; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +public class Format implements VarArgsFunction { + @Override + public String apply(Object[] params) { + return Optional.ofNullable(params).map(p -> { + String format = "yyyy-MM-dd_HH-mm-ss"; + if (p.length > 1) { + format = (String) p[1]; + } + ZonedDateTime zdt; + if (p[0] instanceof String date) { + zdt = ZonedDateTime.parse(date); + } else { + zdt = (ZonedDateTime) p[0]; + } + return DateTimeFormatter.ofPattern(format).format(zdt); + }) + .orElse(""); + } +} diff --git a/common/src/main/java/ctbrec/variableexpansion/functions/Lower.java b/common/src/main/java/ctbrec/variableexpansion/functions/Lower.java new file mode 100644 index 00000000..86b2d6dc --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/functions/Lower.java @@ -0,0 +1,12 @@ +package ctbrec.variableexpansion.functions; + +import ctbrec.variableexpansion.VarArgsFunction; + +import java.util.Optional; + +public class Lower implements VarArgsFunction { + @Override + public String apply(Object[] params) { + return Optional.ofNullable(params).map(sa -> sa[0]).map(String.class::cast).map(String::toLowerCase).orElse(""); + } +} diff --git a/common/src/main/java/ctbrec/variableexpansion/functions/Sanitize.java b/common/src/main/java/ctbrec/variableexpansion/functions/Sanitize.java new file mode 100644 index 00000000..8a700e79 --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/functions/Sanitize.java @@ -0,0 +1,13 @@ +package ctbrec.variableexpansion.functions; + +import ctbrec.StringUtil; +import ctbrec.variableexpansion.VarArgsFunction; + +import java.util.Optional; + +public class Sanitize implements VarArgsFunction { + @Override + public String apply(Object[] params) { + return Optional.ofNullable(params).map(p -> p[0]).map(String.class::cast).map(StringUtil::sanitize).orElse(""); + } +} diff --git a/common/src/main/java/ctbrec/variableexpansion/functions/Trim.java b/common/src/main/java/ctbrec/variableexpansion/functions/Trim.java new file mode 100644 index 00000000..cd67d8d3 --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/functions/Trim.java @@ -0,0 +1,12 @@ +package ctbrec.variableexpansion.functions; + +import ctbrec.variableexpansion.VarArgsFunction; + +import java.util.Optional; + +public class Trim implements VarArgsFunction { + @Override + public String apply(Object[] params) { + return Optional.ofNullable(params).map(sa -> sa[0]).map(String.class::cast).map(String::trim).orElse(""); + } +} diff --git a/common/src/main/java/ctbrec/variableexpansion/functions/Upper.java b/common/src/main/java/ctbrec/variableexpansion/functions/Upper.java new file mode 100644 index 00000000..2d19a817 --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/functions/Upper.java @@ -0,0 +1,12 @@ +package ctbrec.variableexpansion.functions; + +import ctbrec.variableexpansion.VarArgsFunction; + +import java.util.Optional; + +public class Upper implements VarArgsFunction { + @Override + public String apply(Object[] params) { + return Optional.ofNullable(params).map(sa -> sa[0]).map(String.class::cast).map(String::toUpperCase).orElse(""); + } +} diff --git a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java index 6cce755d..b3336321 100644 --- a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java +++ b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java @@ -61,7 +61,7 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { .withLocale(Locale.US) .withZone(ZoneOffset.UTC) .format(rec.getStartDate()); - String input = "asdf_${utcDateTime}_asdf"; + String input = "asdf_$format(${utcDateTime})_asdf"; assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config))); // with user defined pattern @@ -69,7 +69,7 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { .withLocale(Locale.US) .withZone(ZoneOffset.UTC) .format(rec.getStartDate()); - input = "asdf_${utcDateTime(yyyyMMdd-HHmmss)}_asdf"; + input = "asdf_$format(${utcDateTime},yyyyMMdd-HHmmss)_asdf"; assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config))); // multiple occurences with user defined patterns @@ -77,7 +77,7 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { .withLocale(Locale.US) .withZone(ZoneOffset.UTC) .format(rec.getStartDate()); - input = "asdf_${utcDateTime(yyyy)}-${utcDateTime(MM)}-${utcDateTime(dd)}/${utcDateTime(yyyy)}_asdf"; + input = "asdf_$format(${utcDateTime},yyyy)-$format(${utcDateTime},MM)-$format(${utcDateTime},dd)/$format(${utcDateTime},yyyy)_asdf"; assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config))); } @@ -87,14 +87,14 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { .withLocale(Locale.US) .withZone(ZoneId.systemDefault()) .format(rec.getStartDate()); - String input = "asdf_${localDateTime}_asdf"; + String input = "asdf_$format(${localDateTime})_asdf"; assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config))); date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss") .withLocale(Locale.US) .withZone(ZoneId.systemDefault()) .format(rec.getStartDate()); - input = "asdf_${localDateTime(yyyyMMdd-HHmmss)}_asdf"; + input = "asdf_$format(${localDateTime},yyyyMMdd-HHmmss)_asdf"; assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config))); } @@ -142,7 +142,7 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { ctx.getRecording().getModel().setName(null); assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx)); - input = "asdf_${modelGroupName?${utcDateTime(yyyy)?anonymous}}_asdf"; + input = "asdf_${modelGroupName?$format(${utcDateTime},yyyy)}_asdf"; int year = LocalDate.now().getYear(); assertEquals("asdf_" + year + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx)); @@ -160,4 +160,13 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { when(config.getModelNotes(any())).thenReturn(null); assertEquals("asdf__asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config))); } + + @Test + void testFunctionCalls() { + String input = "$upper(${modelName})"; + assertEquals("MOCKITA BOOBILICIOUS", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config))); + + input = "$upper(${doesNotExist?mockita boobilicious})"; + assertEquals("MOCKITA BOOBILICIOUS", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config))); + } }