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)); + } }