Due to convention, interfaces should not have TikTok name inside, but they should have prefix Live instead

- rename TikTokMapper to LiveMapper
- rename TikTokLiveMapperHelper to LiveMapperHelper

Create interface:
LiveEventsHandler for TikTokLiveEventHandler
LiveMessagesHandler for TikTokLiveMessageHandler
This commit is contained in:
jacek.wolniewicz
2024-07-01 23:28:38 +02:00
parent b223651a8f
commit fed9de3fd0
18 changed files with 52 additions and 7 deletions

View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok;
import java.time.Duration;
import java.util.logging.Level;
public class CodeExample {
public static void main(String[] args) {
TikTokLive.newClient("mrBeast")
.onGift((liveClient, event) ->
{
System.out.println("Thank you for Money!");
})
.buildAndConnect();
}
public static void codeExample() {
// <code>
TikTokLive.newClient("bangbetmenygy")
.onGift((liveClient, event) ->
{
String message = switch (event.getGift().getName())
{
case "Rose" -> "ROSE!";
case "Good game" -> "GOOD GAME";
case "Ye" -> "Ye";
case "Nice gift" -> "Nice gift";
default -> "Thank you for " + event.getGift().getName();
};
System.out.println(event.getUser().getProfileName() + " sends " + message);
})
.onGiftCombo((liveClient, event) ->
{
System.out.println(event.getComboState()+ " " + event.getCombo() + " " + event.getGift().getName());
})
.onRoomInfo((liveClient, event) ->
{
var roomInfo = event.getRoomInfo();
System.out.println("Room Id: "+roomInfo.getRoomId());
System.out.println("Likes: "+roomInfo.getLikesCount());
System.out.println("Viewers: "+roomInfo.getViewersCount());
})
.onJoin((liveClient, event) ->
{
System.out.println(event.getUser().getProfileName() + "Hello on my stream! ");
})
.onConnected((liveClient, event) ->
{
System.out.println("Connected to live ");
})
.onError((liveClient, event) ->
{
System.out.println("Error! " + event.getException().getMessage());
})
.buildAndConnect();
// </code>
}
public static void configExample() {
// <code>
TikTokLive.newClient("bangbetmenygy")
.configure((settings) ->
{
settings.setHostName("bangbetmenygy"); // This method is useful in case you want change hostname later
settings.setClientLanguage("en"); // Language
settings.setLogLevel(Level.ALL); // Log level
settings.setPrintToConsole(true); // Printing all logs to console even if log level is Level.OFF
settings.setRetryOnConnectionFailure(true); // Reconnecting if TikTok user is offline
settings.setRetryConnectionTimeout(Duration.ofSeconds(1)); // Timeout before next reconnection
//Optional: Sometimes not every message from chat are send to TikTokLiveJava to fix this issue you can set sessionId
// documentation how to obtain sessionId https://github.com/isaackogan/TikTok-Live-Connector#send-chat-messages
settings.setSessionId("86c3c8bf4b17ebb2d74bb7fa66fd0000");
//Optional:
//RoomId can be used as an override if you're having issues with HostId.
//You can find it in the HTML for the livestream-page
settings.setRoomId("XXXXXXXXXXXXXXXXX");
//Optional:
//API Key for increased limit to signing server
settings.setApiKey("XXXXXXXXXXXXXXXXX");
})
.buildAndConnect();
// </code>
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok;
import java.util.HashMap;
import java.util.regex.Pattern;
public class CodeExamplesGenerator {
public static void main(String[] args) {
var result = new CodeExamplesGenerator().run();
System.out.println(result);
}
public String run() {
var content = FilesUtility.loadFileContent("C:\\Users\\ja\\IdeaProjects\\TikTokLiveJava\\Tools-ReadmeGenerator\\src\\main\\java\\io\\github\\jwdeveloper\\tiktok\\CodeExample.java");
var p = "<code>(.*?)</code>";
var r = Pattern.compile(p, Pattern.DOTALL);
var m = r.matcher(content);
var pattern = """
```java
{{code}}
```
3. Configure (optional)
```java
{{config}}
```
""";
var values = new HashMap<String, Object>();
m.find();
var code = m.group(0)
.replace("<code>", "")
.replace("// </code>", "")
.replaceAll("(?m)^ {8}", "");
values.put("code", code);
m.find();
values.put("config", m.group(1));
var result = TemplateUtility.generateTemplate(pattern, values);
return result;
}
}

View File

@@ -0,0 +1,17 @@
package io.github.jwdeveloper.tiktok;
import io.github.jwdeveloper.descrabble.api.DescriptionDecorator;
import io.github.jwdeveloper.descrabble.api.elements.Element;
import io.github.jwdeveloper.descrabble.api.elements.ElementFactory;
public class EventsDecorator implements DescriptionDecorator {
@Override
public void decorate(Element root, ElementFactory factory)
{
}
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok;
import io.github.jwdeveloper.tiktok.annotations.EventMeta;
import io.github.jwdeveloper.tiktok.annotations.EventType;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.data.events.gift.TikTokGiftEvent;
import io.github.jwdeveloper.tiktok.live.builder.EventsBuilder;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.reflections.Reflections;
import java.util.*;
import java.util.regex.Pattern;
public class EventsInfoGenerator {
public static void main(String[] args) throws ClassNotFoundException {
var res = new EventsInfoGenerator().run();
System.out.println(res);
}
public String run() {
var events = getEventsDtos();
var builder = new StringBuilder();
for (var entry : events.entrySet()) {
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
builder.append(" **" + entry.getKey().name() + "**:").append(System.lineSeparator());
for (var dto : entry.getValue()) {
var link = getLink(dto);
builder.append(System.lineSeparator());
builder.append(link);
}
}
builder.append(System.lineSeparator());
builder.append("# Examples");
builder.append(System.lineSeparator());
for (var entry : events.entrySet()) {
for (var dto : entry.getValue()) {
builder.append("<br>");
builder.append(getMethodContent(dto));
}
}
return builder.toString();
}
public StringBuilder getLink(EventDto dto) {
var sb = new StringBuilder();
var name = dto.getMethodName().toLowerCase()+"-"+dto.getEventClazz().getSimpleName().toLowerCase();
sb.append("- [").append(dto.getMethodName()).append("](#").append(name).append(")");
return sb;
}
public String getMethodContent(EventDto dto) {
var variables = new HashMap<String, Object>();
var doc = getClazzDocumentation(dto.getEventClazz());
variables.put("method-name", dto.getMethodName());
variables.put("content", doc);
variables.put("event-name", dto.getEventClazz().getSimpleName());
var baseUrl = "https://github.com/jwdeveloper/TikTokLiveJava/blob/master/API/src/main/java/";
baseUrl += dto.getEventClazz().getPackageName().replace(".","/");
baseUrl += "/"+dto.getEventClazz().getSimpleName()+".java";
variables.put("event-file-url",baseUrl);
var temp = """
## {{method-name}} [{{event-name}}]({{event-file-url}})
{{content}}
```java
TikTokLive.newClient("host-name")
.{{method-name}}((liveClient, event) ->
{
})
.buildAndConnect();
```
""";
return TemplateUtility.generateTemplate(temp, variables);
}
public String getClazzDocumentation(Class<?> clazz) {
var path = clazz.getName();
path = path.replace(".", "\\");
var fullPath = "C:\\Users\\ja\\IdeaProjects\\TikTokLiveJava\\API\\src\\main\\java\\" + path + ".java";
var content = FilesUtility.loadFileContent(fullPath);
var index = content.indexOf(" */");
content = content.substring(index+4);
String pattern = "(?s)\\/\\\\*(.*?)\\*\\/";
var r = Pattern.compile(pattern);
var m = r.matcher(content);
if (!m.find()) {
return "";
}
var group = m.group(1);
var reuslt = group.replace("*","").replaceAll("\\*", "");
return reuslt;
}
public Map<EventType, List<EventDto>> getEventsDtos(){
var result = new TreeMap<EventType, List<EventDto>>();
var baseClazz = EventsBuilder.class;
var reflections = new Reflections("io.github.jwdeveloper.tiktok.data.events");
var classes = reflections.getSubTypesOf(TikTokEvent.class);
var methods = baseClazz.getDeclaredMethods();
for (var method : methods) {
if (method.getName().equals("onEvent")) {
var dto = new EventDto(EventType.Message, "onEvent", TikTokEvent.class);
result.computeIfAbsent(EventType.Message, eventType -> new ArrayList<>()).add(dto);
continue;
}
var parsedName = method.getName().replaceFirst("on", "");
var name = "TikTok" + parsedName + "Event";
var optional = classes.stream().filter(e -> e.getSimpleName().equals(name)).findFirst();
if (optional.isEmpty()) {
System.out.println("Not found!: " + name);
continue;
}
var clazz = optional.get();
var annotation = clazz.getAnnotation(EventMeta.class);
var dto = new EventDto(annotation.eventType(), method.getName(), clazz);
result.computeIfAbsent(dto.eventType, eventType -> new ArrayList<>()).add(dto);
}
return result;
}
@AllArgsConstructor
@Getter
public static final class EventDto {
private EventType eventType;
private String methodName;
private Class eventClazz;
@Override
public String toString() {
return "EventDto{" +
"eventType=" + eventType +
", methodName='" + methodName + '\'' +
", eventClazz=" + eventClazz.getSimpleName() +
'}';
}
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class FilesUtility
{
public static List<Path> getFiles(String input) {
Path path = Paths.get(input);
try (Stream<Path> paths = Files.list(path)) {
return paths.filter(Files::isRegularFile).toList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static String getFileFromResource(Class clazz, String path)
{
try {
var stream =clazz.getClassLoader().getResourceAsStream(path);
var bytes= stream.readAllBytes();
stream.close();
return new String(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static List<Path> getFileContent(String input) {
Path path = Paths.get(input);
try (Stream<Path> paths = Files.list(path)) {
return paths.filter(Files::isRegularFile).toList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void saveFile(String path, String content)
{
Path filePath = Paths.get(path);
try {
// Write the content to the file
Files.write(filePath, content.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
public static boolean pathExists(String path) {
var directory = new File(path);
return directory.exists();
}
public static File ensurePath(String path) {
var directory = new File(path);
if (directory.exists()) {
return directory;
}
directory.mkdirs();
return directory;
}
public static void ensureFile(String paths) {
var file = new File(paths);
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
public static String loadFileContent(String path) {
ensureFile(path);
Path pathh = Paths.get(path);
try {
return new String(Files.readAllBytes(pathh));
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok;
import java.util.regex.Pattern;
public class ListenerExampleGenerator
{
public static void main(String[] arg)
{
var result = new ListenerExampleGenerator();
System.out.println(result);
}
public String run()
{
var content = FilesUtility.loadFileContent("C:\\Users\\ja\\IdeaProjects\\TikTokLiveJava\\Examples\\src\\main\\java\\io\\github\\jwdeveloper\\tiktok\\ListenerExample.java");
var p = "<code>(.*?)</code>";
var r = Pattern.compile(p, Pattern.DOTALL);
var m = r.matcher(content);
m.find();
return m.group(1);
}
}

View File

@@ -0,0 +1,38 @@
package io.github.jwdeveloper.tiktok;
import io.github.jwdeveloper.descrabble.api.DescriptionGenerator;
import io.github.jwdeveloper.descrabble.framework.Descrabble;
import io.github.jwdeveloper.descrabble.plugin.github.DescrabbleGithub;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
public class Main
{
public static void main(String[] args) throws IOException {
var version = System.getenv("VERSION");
if (version == null || version.equals("")) {
version = "[Replace with current version]";
}
var inputStream = Main.class.getResourceAsStream("/readme-template.html");
var targetFile = new File("temp.file");
FileUtils.copyInputStreamToFile(inputStream, targetFile);
var output = System.getProperty("user.dir");
DescriptionGenerator generator = Descrabble.create()
.withTemplate(targetFile)
.withVariable("version", version)
.withDecorator(new EventsDecorator())
.withPlugin(DescrabbleGithub.plugin("README.md"))
.build();
generator.generate(output);
targetFile.delete();
inputStream.close();
}
}

View File

@@ -0,0 +1,27 @@
package io.github.jwdeveloper.tiktok;
import com.google.common.base.Charsets;
import com.google.common.collect.Maps;
import com.google.common.io.Resources;
import com.hubspot.jinjava.Jinjava;
import java.io.IOException;
import java.util.HashMap;
public class Main2 {
public static void main(String[] args) throws IOException {
var version = System.getenv("VERSION");
if (version == null || version.equals("")) {
version = "[Replace with current version]";
}
var template = Resources.toString(Resources.getResource("my-template.html"), Charsets.UTF_8);
var jinjava = new Jinjava();
var context = new HashMap<String, Object>();
context.put("version", version);
var renderedTemplate = jinjava.render(template, context);
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok;
import java.util.HashMap;
public class ReadmeGenerator {
public static void main(String[] args) {
var generator = new ReadmeGenerator();
generator.generate();
}
public void generate() {
var template = FilesUtility.getFileFromResource(ReadmeGenerator.class, "template.md");
var variables = new HashMap<String, Object>();
variables.put("version", getCurrentVersion());
variables.put("code-content", new CodeExamplesGenerator().run());
variables.put("events-content", new EventsInfoGenerator().run());
variables.put("listener-content",new ListenerExampleGenerator().run());
template = TemplateUtility.generateTemplate(template, variables);
var outputPath = "C:\\Users\\ja\\IdeaProjects\\TikTokLiveJava\\Tools-ReadmeGenerator\\src\\main\\resources\\output.md";
FilesUtility.saveFile(outputPath, template);
}
public String getCurrentVersion() {
var version = System.getenv("version");
;
return version == null ? "NOT_FOUND" : version;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok;
import java.util.Map;
public class TemplateUtility {
public static String generateTemplate(String template, Map<String, Object> values) {
for (var entry : values.entrySet()) {
template = doReplacement(template, entry.getKey(), entry.getValue().toString());
}
return template;
}
private static String doReplacement(String template, String keyword, String value) {
var key = "(\\{\\{)" + keyword + "(}})";
return template.replaceAll(key, value);
}
}