From c49a7db192bbea0e003d722217870ea2b885f9d2 Mon Sep 17 00:00:00 2001
From: 0xb00bface <0xboobface@gmail.com>
Date: Sun, 9 May 2021 21:28:22 +0200
Subject: [PATCH] Add mechanism to specify fallback values in the pp variables
---
.../resources/html/docs/PostProcessing.md | 16 +-
...AbstractPlaceholderAwarePostProcessor.java | 200 +++++++++++-------
...ractPlaceholderAwarePostProcessorTest.java | 18 ++
3 files changed, 156 insertions(+), 78 deletions(-)
diff --git a/client/src/main/resources/html/docs/PostProcessing.md b/client/src/main/resources/html/docs/PostProcessing.md
index 9fdc86b4..9cc13eeb 100644
--- a/client/src/main/resources/html/docs/PostProcessing.md
+++ b/client/src/main/resources/html/docs/PostProcessing.md
@@ -42,11 +42,14 @@ The part you have to copy is
+###### Available variables:
- **${modelName}** - the name of the recorded model
- **${modelDisplayName}** - the name of the recorded model, which is shown on the webpage. Might be the same as
${modelName}
- **${modelSanitizedName}** - sanitized name of the model. The following characters are replaced by an underscore:
\\, /, ', " and space
+- **${modelGroupName}** - name of the model group, if the model is part of a group
+- **${modelGroupId}** - the unique ID of the model group, if the model is part of a group
- **${siteName}** - the name of the cam site, the model streams on
- **${siteSanitizedName}** - sanitized name of the site. The following characters are replaced by an underscore:
\\, /, ', " and space
@@ -116,4 +119,15 @@ The part you have to copy is
- For more information see: [DateTimeFormatter](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html)
\ No newline at end of file
+ For more information see: [DateTimeFormatter](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html)
+###### Fallback values:
+You can define a fallback value for each variable in case there is no value available for the variable. The syntax is
+
+ ${placeholder?foobar}
+
+Let's for example say you have created some model groups. For models, which are part of a group, you want to use the group name. But for models, which
+are not part of a group you want to use the sanitized name. You can achieve that by using the following expression:
+
+ ${modelGroupName?${modelSanitizedName}}
+
+It can be read like "use the modelGroupName, but if that is not available use modelSanitizedName".
\ No newline at end of file
diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java
index 40e79aec..95f2cc52 100644
--- a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java
+++ b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java
@@ -3,107 +3,153 @@ package ctbrec.recorder.postprocessing;
import static ctbrec.StringUtil.*;
import static java.util.Optional.*;
+import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Function;
import ctbrec.Config;
+import ctbrec.Model;
+import ctbrec.ModelGroup;
import ctbrec.Recording;
+import ctbrec.StringUtil;
import ctbrec.sites.Site;
public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor {
- public static final String[] PLACE_HOLDERS = {
- "${modelName}",
- "${modelDisplayName}",
- "${modelSanitizedName}",
- "${siteName}",
- "${siteSanitizedName}",
- "${utcDateTime}",
- "${localDateTime}",
- "${epochSecond}",
- "${fileSuffix}",
- "${modelNotes}",
- "${recordingNotes}",
- "${recordingsDir}",
- "${absolutePath}",
- "${absoluteParentPath}"
- };
-
public String fillInPlaceHolders(String input, PostProcessingContext ctx) {
Recording rec = ctx.getRecording();
Config config = ctx.getConfig();
- // @formatter:off
- String output = input
- .replace("${modelName}", ofNullable(rec.getModel().getName()).orElse("modelName"))
- .replace("${modelDisplayName}", ofNullable(rec.getModel().getDisplayName()).orElse("displayName"))
- .replace("${modelSanitizedName}", ofNullable(rec.getModel().getSanitizedNamed()).orElse("sanitizedName"))
- .replace("${siteName}", ofNullable(rec.getModel().getSite()).map(Site::getName).orElse("site"))
- .replace("${siteSanitizedName}", getSanitizedSiteName(rec))
- .replace("${fileSuffix}", getFileSuffix(rec))
- .replace("${epochSecond}", Long.toString(rec.getStartDate().getEpochSecond()))
- .replace("${modelNotes}", sanitize(config.getModelNotes(rec.getModel())))
- .replace("${recordingNotes}", getSanitizedRecordingNotes(rec))
- .replace("${recordingsDir}", config.getSettings().recordingsDir)
- .replace("${absolutePath}", rec.getPostProcessedFile().getAbsolutePath())
- .replace("${absoluteParentPath}", rec.getPostProcessedFile().getParentFile().getAbsolutePath())
- ;
+ Optional modelGroup = config.getModelGroup(rec.getModel());
- output = replaceUtcDateTime(rec, output);
- output = replaceLocalDateTime(rec, output);
+ Map>> placeholderValueSuppliers = new HashMap<>();
+ placeholderValueSuppliers.put("modelName", r -> ofNullable(rec.getModel().getName()));
+ placeholderValueSuppliers.put("modelDisplayName", r -> ofNullable(rec.getModel().getDisplayName()));
+ placeholderValueSuppliers.put("modelSanitizedName", r -> getSanitizedName(rec.getModel()));
+ placeholderValueSuppliers.put("siteName", r -> ofNullable(rec.getModel().getSite()).map(Site::getName));
+ placeholderValueSuppliers.put("siteSanitizedName", r -> getSanitizedSiteName(rec));
+ placeholderValueSuppliers.put("fileSuffix", r -> getFileSuffix(rec));
+ placeholderValueSuppliers.put("epochSecond", r -> ofNullable(rec.getStartDate()).map(Instant::getEpochSecond).map(l -> Long.toString(l))); // NOSONAR
+ placeholderValueSuppliers.put("modelNotes", r -> getSanitizedModelNotes(config, rec.getModel()));
+ placeholderValueSuppliers.put("recordingNotes", r -> getSanitizedRecordingNotes(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("modelGroupName", r -> modelGroup.map(ModelGroup::getName));
+ placeholderValueSuppliers.put("modelGroupId", r -> modelGroup.map(ModelGroup::getId).map(UUID::toString));
+ placeholderValueSuppliers.put("utcDateTime", pattern -> replaceUtcDateTime(rec, pattern));
+ placeholderValueSuppliers.put("localDateTime", pattern -> replaceLocalDateTime(rec, pattern));
+ String output = fillInPlaceHolders(input, placeholderValueSuppliers);
return output;
- // @formatter:on
}
- private String replaceUtcDateTime(Recording rec, String filename) {
- return replaceDateTime(rec, filename, "utcDateTime", ZoneOffset.UTC);
- }
-
- private String replaceLocalDateTime(Recording rec, String filename) {
- return replaceDateTime(rec, filename, "localDateTime", ZoneId.systemDefault());
- }
-
- private String replaceDateTime(Recording rec, String filename, String placeHolder, ZoneId zone) {
- String pattern = "yyyy-MM-dd_HH-mm-ss";
- Pattern regex = Pattern.compile("\\$\\{" + placeHolder + "(?:\\((.*?)\\))?\\}");
- Matcher m = regex.matcher(filename);
- while (m.find()) {
- String p = m.group(1);
- if (p != null) {
- pattern = p;
- }
- String formattedDate = getDateTime(rec, pattern, zone);
- filename = m.replaceFirst(formattedDate);
- m = regex.matcher(filename);
- }
- return filename;
- }
-
- private String getDateTime(Recording rec, String pattern, ZoneId zone) {
- return DateTimeFormatter.ofPattern(pattern)
- .withLocale(Locale.getDefault())
- .withZone(zone)
- .format(rec.getStartDate());
- }
-
- private CharSequence getFileSuffix(Recording rec) {
- if(rec.isSingleFile()) {
- String filename = rec.getPostProcessedFile().getName();
- return filename.substring(filename.lastIndexOf('.') + 1);
+ private Optional getSanitizedName(Model model) {
+ String name = model.getSanitizedNamed();
+ if (StringUtil.isBlank(name)) {
+ return Optional.empty();
} else {
- return "";
+ return Optional.of(name);
}
}
- private CharSequence getSanitizedSiteName(Recording rec) {
- return sanitize(ofNullable(rec.getModel().getSite()).map(Site::getName).orElse(""));
+ private 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);
+ }
+ 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;
+ }
+ }
+ } while (somethingReplaced);
+ return input;
}
- private CharSequence getSanitizedRecordingNotes(Recording rec) {
- return sanitize(ofNullable(rec.getNote()).orElse(""));
+ private Optional replaceUtcDateTime(Recording rec, String pattern) {
+ return replaceDateTime(rec, pattern, ZoneOffset.UTC);
+ }
+
+ private Optional replaceLocalDateTime(Recording rec, String filename) {
+ return replaceDateTime(rec, filename, 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) {
+ if (rec.isSingleFile()) {
+ String filename = rec.getPostProcessedFile().getName();
+ return Optional.of(filename.substring(filename.lastIndexOf('.') + 1));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private Optional getSanitizedSiteName(Recording rec) {
+ Optional name = ofNullable(rec.getModel().getSite()).map(Site::getName);
+ if (name.isPresent()) {
+ return Optional.of(sanitize(name.get()));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private Optional getSanitizedRecordingNotes(Recording rec) {
+ Optional notes = ofNullable(rec.getNote());
+ if (notes.isPresent()) {
+ return Optional.of(sanitize(notes.get()));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ 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();
+ }
}
}
diff --git a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java
index bf66f978..06f91a45 100644
--- a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java
+++ b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java
@@ -132,4 +132,22 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
String input = "asdf_${modelNotes}_asdf";
assertEquals("asdf_tag,_foo,_bar_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
}
+
+ @Test
+ public void testPlaceholderDefaultValues() throws IOException {
+ String input = "asdf_${modelGroupName?${modelSanitizedName?anonymous}}_asdf";
+ PostProcessingContext ctx = createPostProcessingContext(rec, null, config);
+ ctx.getRecording().getModel().setName(null);
+ assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
+
+ input = "asdf_${modelGroupName?${utcDateTime(yyyy)?anonymous}}_asdf";
+ assertEquals("asdf_2021_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
+
+ ctx.getRecording().setStartDate(null);
+ input = "asdf_${modelGroupName?${utcDateTime(yyyy)?anonymous}}_asdf";
+ assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
+
+ input = "asdf_${modelGroupName?${utcDateTime?anonymous}}_asdf";
+ assertEquals("asdf_anonymous_asdf", placeHolderAwarePp.fillInPlaceHolders(input, ctx));
+ }
}