From ed5d674be3378dc96171d7e6f05466fcafa5a7f1 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Fri, 10 Sep 2021 14:30:49 +0200 Subject: [PATCH] Add variable support for model variable to player params --- CHANGELOG.md | 7 + client/src/main/java/ctbrec/ui/Player.java | 125 ++++++++++-------- .../java/ctbrec/ui/settings/SettingsTab.java | 45 +++++-- common/src/main/java/ctbrec/StringUtil.java | 17 +++ ...AbstractPlaceholderAwarePostProcessor.java | 54 ++------ .../AbstractVariableExpander.java | 59 +++++++++ .../ModelVariableExpander.java | 60 +++++++++ 7 files changed, 254 insertions(+), 113 deletions(-) create mode 100644 common/src/main/java/ctbrec/variableexpansion/AbstractVariableExpander.java create mode 100644 common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 3927b3c5..1ddd5f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ * Fix LiveJasmin followed tab * Add buttons to settings to delete cookies per site * Fix bug in minimal browser +* Model placeholders can now be used for player params + ${modelName} + ${modelDisplayName} + ${modelSanitizedName} + ${modelNotes} + ${siteName} + ${siteSanitizedName} 4.5.3 ======================== diff --git a/client/src/main/java/ctbrec/ui/Player.java b/client/src/main/java/ctbrec/ui/Player.java index 34671404..c0cd1689 100644 --- a/client/src/main/java/ctbrec/ui/Player.java +++ b/client/src/main/java/ctbrec/ui/Player.java @@ -25,12 +25,14 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.OS; import ctbrec.Recording; +import ctbrec.StringUtil; import ctbrec.event.EventBusHolder; import ctbrec.io.StreamRedirector; import ctbrec.io.UrlUtil; import ctbrec.recorder.download.StreamSource; import ctbrec.ui.controls.Dialogs; import ctbrec.ui.event.PlayerStartedEvent; +import ctbrec.variableexpansion.ModelVariableExpander; import javafx.scene.Scene; public class Player { @@ -41,24 +43,6 @@ public class Player { private Player() { } - private static boolean play(String url, boolean async) { - boolean singlePlayer = Config.getInstance().getSettings().singlePlayer; - try { - if (singlePlayer && playerThread != null && playerThread.isRunning()) { - playerThread.stopThread(); - } - - playerThread = new PlayerThread(url); - if (!async) { - playerThread.join(); - } - return true; - } catch (Exception e1) { - LOG.error("Couldn't start player", e1); - return false; - } - } - public static boolean play(Recording rec) { boolean singlePlayer = Config.getInstance().getSettings().singlePlayer; try { @@ -85,44 +69,34 @@ public class Player { if (singlePlayer && playerThread != null && playerThread.isRunning()) { playerThread.stopThread(); } - String playlistUrl = getPlaylistUrl(model); - LOG.debug("Playing {}", playlistUrl); + EventBusHolder.BUS.post(new PlayerStartedEvent(model)); - return Player.play(playlistUrl, async); + + if (singlePlayer && playerThread != null && playerThread.isRunning()) { + playerThread.stopThread(); + } + + playerThread = new PlayerThread(model); + if (!async) { + playerThread.join(); + } + return true; } else { Dialogs.showError(scene, "Room not public", "Room is currently not public", null); return false; } - } catch (Exception e1) { - LOG.error("Couldn't get stream information for model {}", model, e1); - Dialogs.showError(scene, "Couldn't determine stream URL", e1.getLocalizedMessage(), e1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.error("Couldn't get stream information for model {}", model, e); + Dialogs.showError(scene, "Couldn't determine stream URL", e.getLocalizedMessage(), e); + return false; + } catch (Exception e) { + LOG.error("Couldn't get stream information for model {}", model, e); + Dialogs.showError(scene, "Couldn't determine stream URL", e.getLocalizedMessage(), e); return false; } } - private static String getPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException { - List sources = model.getStreamSources(); - Collections.sort(sources); - StreamSource best; - int maxRes = Config.getInstance().getSettings().maximumResolutionPlayer; - if (maxRes > 0 && !sources.isEmpty()) { - for (Iterator iterator = sources.iterator(); iterator.hasNext();) { - StreamSource streamSource = iterator.next(); - if (streamSource.height > 0 && maxRes < streamSource.height) { - LOG.trace("Res too high {} > {}", streamSource.height, maxRes); - iterator.remove(); - } - } - } - if (sources.isEmpty()) { - throw new RuntimeException("No stream left in playlist, because player resolution is set to " + maxRes); - } else { - LOG.debug("{} selected {}", model.getName(), sources.get(sources.size() - 1)); - best = sources.get(sources.size() - 1); - } - return best.getMediaPlaylistUrl(); - } - public static void stop() { if (playerThread != null) { playerThread.stopThread(); @@ -132,11 +106,11 @@ public class Player { private static class PlayerThread extends Thread { private boolean running = false; private Process playerProcess; - private String url; private Recording rec; + private Model model; - PlayerThread(String url) { - this.url = url; + PlayerThread(Model model) { + this.model = model; setName(getClass().getName()); start(); } @@ -158,11 +132,16 @@ public class Player { String[] cmdline = createCmdline(file.getAbsolutePath()); playerProcess = rt.exec(cmdline, OS.getEnvironment(), file.getParentFile()); } else { + String url = null; if (rec != null) { url = getRemoteRecordingUrl(rec, cfg); + } else if (model != null) { + url = getPlaylistUrl(model); } LOG.debug("Playing {}", url); String[] cmdline = createCmdline(url); + expandPlaceHolders(cmdline); + LOG.debug("Player command line: {}", Arrays.toString(cmdline)); playerProcess = rt.exec(cmdline); } @@ -179,6 +158,10 @@ public class Player { playerProcess.waitFor(); LOG.debug("Media player finished."); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.error("Error in player thread", e); + Dialogs.showError(scene, "Playback failed", "Couldn't start playback", e); } catch (Exception e) { LOG.error("Error in player thread", e); Dialogs.showError(scene, "Playback failed", "Couldn't start playback", e); @@ -186,20 +169,52 @@ public class Player { running = false; } + private static String getPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException { + List sources = model.getStreamSources(); + Collections.sort(sources); + StreamSource best; + int maxRes = Config.getInstance().getSettings().maximumResolutionPlayer; + if (maxRes > 0 && !sources.isEmpty()) { + for (Iterator iterator = sources.iterator(); iterator.hasNext();) { + StreamSource streamSource = iterator.next(); + if (streamSource.height > 0 && maxRes < streamSource.height) { + LOG.trace("Res too high {} > {}", streamSource.height, maxRes); + iterator.remove(); + } + } + } + if (sources.isEmpty()) { + throw new RuntimeException("No stream left in playlist, because player resolution is set to " + maxRes); + } else { + LOG.debug("{} selected {}", model.getName(), sources.get(sources.size() - 1)); + best = sources.get(sources.size() - 1); + } + return best.getMediaPlaylistUrl(); + } + + private void expandPlaceHolders(String[] cmdline) { + ModelVariableExpander expander = new ModelVariableExpander(model, Config.getInstance(), null); + for (int i = 0; i < cmdline.length; i++) { + var param = cmdline[i]; + param = expander.expand(param); + cmdline[i] = param; + } + } + private String[] createCmdline(String mediaSource) { Config cfg = Config.getInstance(); String params = cfg.getSettings().mediaPlayerParams.trim(); + String[] cmdline = null; - if(!params.isEmpty()) { - String[] playerArgs = params.split(" "); + if (params.isEmpty()) { + cmdline = new String[2]; + } else { + String[] playerArgs = StringUtil.splitParams(params); cmdline = new String[playerArgs.length + 2]; System.arraycopy(playerArgs, 0, cmdline, 1, playerArgs.length); - } else { - cmdline = new String[2]; } cmdline[0] = cfg.getSettings().mediaPlayer; cmdline[cmdline.length - 1] = mediaSource; - LOG.debug("Player command line: {}", Arrays.toString(cmdline)); return cmdline; } diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 25af4367..b13febf2 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -225,20 +225,25 @@ public class SettingsTab extends Tab implements TabSelectionListener { var storage = new CtbrecPreferencesStorage(config); var prefs = Preferences.of(storage, Category.of("General", - Group.of("General", Setting.of("User-Agent", httpUserAgent), + Group.of("General", + Setting.of("User-Agent", httpUserAgent), Setting.of("User-Agent mobile", httpUserAgentMobile), Setting.of("Update overview interval (seconds)", overviewUpdateIntervalInSecs, "Update the thumbnail overviews every x seconds").needsRestart(), Setting.of("Update thumbnails", updateThumbnails, "The overviews will still be updated, but the thumbnails won't be changed. This is useful for less powerful systems."), Setting.of("Manually select stream quality", chooseStreamQuality, "Opens a dialog to select the video resolution before recording"), - Setting.of("Enable live previews (experimental)", livePreviews), Setting.of("Enable recently watched tab", recentlyWatched).needsRestart(), + Setting.of("Enable live previews (experimental)", livePreviews), + Setting.of("Enable recently watched tab", recentlyWatched).needsRestart(), Setting.of("Minimize to tray", minimizeToTray, "Removes the app from the task bar, if minimized"), Setting.of("Add models from clipboard", monitorClipboard, "Monitor clipboard for model URLs and automatically add them to the recorder").needsRestart(), Setting.of("Show confirmation dialogs", confirmationDialogs, "Show confirmation dialogs for irreversible actions"), Setting.of("Start Tab", startTab)), - Group.of("Player", Setting.of("Player", mediaPlayer), Setting.of("Start parameters", mediaPlayerParams), + Group.of("Player", + Setting.of("Player", mediaPlayer), + Setting.of("Start parameters", mediaPlayerParams), Setting.of("Maximum resolution (0 = unlimited)", maximumResolutionPlayer, "video height, e.g. 720 or 1080"), - Setting.of("Show \"Player Starting\" Message", showPlayerStarting), Setting.of("Start only one player at a time", singlePlayer))), + Setting.of("Show \"Player Starting\" Message", showPlayerStarting), + Setting.of("Start only one player at a time", singlePlayer))), Category.of("Look & Feel", Group.of("Look & Feel", Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance())).needsRestart(), @@ -248,10 +253,11 @@ public class SettingsTab extends Tab implements TabSelectionListener { Setting.of("Show grid lines in tables", showGridLinesInTables, "Show grid lines in tables").needsRestart(), Setting.of("Fast scroll speed", fastScrollSpeed, "Makes the thumbnail overviews scroll faster with the mouse wheel").needsRestart())), Category.of("Recorder", - Group.of("Recorder", Setting.of("Recordings Directory", recordingsDir), Setting.of("Directory Structure", directoryStructure), + Group.of("Recorder", + Setting.of("Recordings Directory", recordingsDir), + Setting.of("Directory Structure", directoryStructure), Setting.of("Split recordings after", splitAfter).converter(SplitAfterOption.converter()).onChange(this::splitValuesChanged), - Setting.of("Split recordings bigger than", splitBiggerThan).converter(SplitBiggerThanOption.converter()) - .onChange(this::splitValuesChanged), + Setting.of("Split recordings bigger than", splitBiggerThan).converter(SplitBiggerThanOption.converter()).onChange(this::splitValuesChanged), Setting.of("Restrict Resolution", resolutionRange, "Only record streams with resolution within the given range"), Setting.of("Concurrent Recordings (0 = unlimited)", concurrentRecordings), Setting.of("Default Priority", defaultPriority), @@ -265,23 +271,34 @@ public class SettingsTab extends Tab implements TabSelectionListener { Setting.of("Don't record from", timeoutRecordingStartingAt), Setting.of("Until", timeoutRecordingEndingAt) ), - Group.of("Location", Setting.of("Record Location", recordLocal).needsRestart(), Setting.of("Server", server), Setting.of("Port", port), + Group.of("Location", + Setting.of("Record Location", recordLocal).needsRestart(), + Setting.of("Server", server), + Setting.of("Port", port), Setting.of("Path", path, "Leave empty, if you didn't change the servletContext in the server config"), - Setting.of("Download Filename", downloadFilename, "File name pattern for downloads"), Setting.of("", variablesHelpButton), + Setting.of("Download Filename", downloadFilename, "File name pattern for downloads"), + Setting.of("", variablesHelpButton), Setting.of("Require authentication", requireAuthentication), Setting.of("Use Secure Communication (TLS)", transportLayerSecurity))), Category.of("Post-Processing", - Group.of("Post-Processing", Setting.of("Threads", postProcessingThreads), Setting.of("Steps", postProcessingStepPanel), + Group.of("Post-Processing", + Setting.of("Threads", postProcessingThreads), + Setting.of("Steps", postProcessingStepPanel), Setting.of("", createHelpButton("Post-Processing Help", "http://localhost:5689/docs/PostProcessing.md")))), Category.of("Events & Actions", new ActionSettingsPanel(recorder)), Category.of("Ignore List", ignoreList), Category.of("Sites", siteCategories.toArray(new Category[0])), Category.of("Proxy", - Group.of("Proxy", Setting.of("Type", proxyType).needsRestart(), Setting.of("Host", proxyHost).needsRestart(), - Setting.of("Port", proxyPort).needsRestart(), Setting.of("Username", proxyUser).needsRestart(), + Group.of("Proxy", + Setting.of("Type", proxyType).needsRestart(), + Setting.of("Host", proxyHost).needsRestart(), + Setting.of("Port", proxyPort).needsRestart(), + Setting.of("Username", proxyUser).needsRestart(), Setting.of("Password", proxyPassword).needsRestart())), Category.of("Advanced / Devtools", - Group.of("Networking", Setting.of("Playlist request timeout (ms)", playlistRequestTimeout, "Timeout in ms for playlist requests")), - Group.of("Logging", Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory"), + Group.of("Networking", + Setting.of("Playlist request timeout (ms)", playlistRequestTimeout, "Timeout in ms for playlist requests")), + Group.of("Logging", + Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory"), Setting.of("Log missed segments", logMissedSegments, "Write a log files in the system's temp directory to analyze missed segments")), Group.of("hlsdl (experimental)", diff --git a/common/src/main/java/ctbrec/StringUtil.java b/common/src/main/java/ctbrec/StringUtil.java index f117accc..c1c93596 100644 --- a/common/src/main/java/ctbrec/StringUtil.java +++ b/common/src/main/java/ctbrec/StringUtil.java @@ -1,7 +1,10 @@ package ctbrec; import java.text.DecimalFormat; +import java.util.LinkedList; +import java.util.List; import java.util.StringTokenizer; +import java.util.regex.Matcher; import java.util.regex.Pattern; public class StringUtil { @@ -191,4 +194,18 @@ public class StringUtil { } return a; } + + public static String[] splitParams(String params) { + Pattern p = Pattern.compile("(\"[^\"]+\"|[^\\s\"]+)"); + Matcher m = p.matcher(params); + List result = new LinkedList<>(); + while (m.find()) { + String group = m.group(); + if (group.startsWith("\"") && group.endsWith("\"")) { + group = group.substring(1, group.length() - 1); + } + result.add(group); + } + return result.toArray(new String[0]); + } } diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java index 410b7702..31ebd108 100644 --- a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java +++ b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java @@ -12,38 +12,31 @@ import java.util.Locale; 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; +import ctbrec.variableexpansion.ModelVariableExpander; public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPostProcessor { public String fillInPlaceHolders(String input, PostProcessingContext ctx) { Recording rec = ctx.getRecording(); Config config = ctx.getConfig(); - Optional modelGroup = Optional.ofNullable(ctx.getRecorder()).flatMap(r -> r.getModelGroup(rec.getModel())); 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())); + + ModelVariableExpander modelExpander = new ModelVariableExpander(rec.getModel(), config, ctx.getRecorder()); + placeholderValueSuppliers.putAll(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("modelGroupName", r -> modelGroup.map(ModelGroup::getName)); - placeholderValueSuppliers.put("modelGroupId", r -> modelGroup.map(ModelGroup::getId).map(UUID::toString)); + + 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)); @@ -51,15 +44,6 @@ public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPost return output; } - private Optional getSanitizedName(Model model) { - String name = model.getSanitizedNamed(); - if (StringUtil.isBlank(name)) { - return Optional.empty(); - } else { - return Optional.of(name); - } - } - private String fillInPlaceHolders(String input, Map>> placeholderValueSuppliers) { boolean somethingReplaced = false; do { @@ -89,7 +73,7 @@ public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPost 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(); + String output = sb.replace(start, end + 1, value).toString(); somethingReplaced = !Objects.equals(input, output); input = output; } @@ -128,15 +112,6 @@ public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPost } } - 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()) { @@ -145,13 +120,4 @@ public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPost 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/main/java/ctbrec/variableexpansion/AbstractVariableExpander.java b/common/src/main/java/ctbrec/variableexpansion/AbstractVariableExpander.java new file mode 100644 index 00000000..ae528d6a --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/AbstractVariableExpander.java @@ -0,0 +1,59 @@ +package ctbrec.variableexpansion; + +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; + +abstract class AbstractVariableExpander { + + 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; + } + } + } while (somethingReplaced); + return input; + } + + public Set getPlaceholderNames() { + return placeholderValueSuppliers.keySet(); + } + + 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 new file mode 100644 index 00000000..2e876fe7 --- /dev/null +++ b/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java @@ -0,0 +1,60 @@ +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; +import ctbrec.StringUtil; +import ctbrec.recorder.Recorder; +import ctbrec.sites.Site; + +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.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)); + } + + public String expand(String input) { + return fillInPlaceHolders(input, placeholderValueSuppliers); + } + + private Optional getSanitizedName(Model model) { + String name = model.getSanitizedNamed(); + if (StringUtil.isBlank(name)) { + return Optional.empty(); + } else { + return Optional.of(name); + } + } + + private Optional getSanitizedSiteName(Model model) { + Optional name = ofNullable(model.getSite()).map(Site::getName); + if (name.isPresent()) { + return Optional.of(sanitize(name.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(); + } + } +}