Compare commits

...

3 Commits

Author SHA1 Message Date
d47e43edb1 support included configurations
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-04 23:58:22 +03:00
c930070b9c add flow: actions + screens 2021-08-30 23:34:57 +03:00
81cac78737 implement toString for keyboard 2021-08-30 23:23:49 +03:00
27 changed files with 736 additions and 313 deletions

View File

@@ -1,24 +1,13 @@
package ru.penkrat.stbf.impl.pengrad; package ru.penkrat.stbf.impl.pengrad;
import java.lang.reflect.Field;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.gson.internal.reflect.ReflectionAccessor;
import com.pengrad.telegrambot.TelegramBot; import com.pengrad.telegrambot.TelegramBot;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import com.pengrad.telegrambot.model.request.*;
import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup;
import com.pengrad.telegrambot.model.request.KeyboardButton;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.model.request.ReplyKeyboardMarkup;
import com.pengrad.telegrambot.request.DeleteMessage; import com.pengrad.telegrambot.request.DeleteMessage;
import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.EditMessageText;
import com.pengrad.telegrambot.request.SendDocument; import com.pengrad.telegrambot.request.SendDocument;
import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.request.SendMessage;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.BotResponse; import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.Screen; import ru.penkrat.stbf.api.Screen;
@@ -45,11 +34,13 @@ public class BotResponseImpl implements BotResponse {
KeyboardButton[][] keyboard = kk.getKeyboard(); KeyboardButton[][] keyboard = kk.getKeyboard();
InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard(); InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard();
if (log.isDebugEnabled()) {
log.debug("{}", kk.toFriendlyString());
}
if (inlineKeyboard != null && inlineKeyboard.length > 0) { if (inlineKeyboard != null && inlineKeyboard.length > 0) {
logKeyboard(inlineKeyboard);
sendMessage = sendMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard)); sendMessage = sendMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard));
} else if (keyboard != null && keyboard.length > 0) { } else if (keyboard != null && keyboard.length > 0) {
logKeyboard(keyboard);
sendMessage = sendMessage.replyMarkup(new ReplyKeyboardMarkup(keyboard)); sendMessage = sendMessage.replyMarkup(new ReplyKeyboardMarkup(keyboard));
} else { } else {
log.debug("No keyboard"); log.debug("No keyboard");
@@ -82,8 +73,11 @@ public class BotResponseImpl implements BotResponse {
KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard(); KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard();
InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard(); InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard();
if (log.isDebugEnabled()) {
log.debug("{}", kk.toFriendlyString());
}
if (inlineKeyboard != null && inlineKeyboard.length > 0) { if (inlineKeyboard != null && inlineKeyboard.length > 0) {
logKeyboard(inlineKeyboard);
editMessage = editMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard)); editMessage = editMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard));
} else { } else {
log.debug("No keyboard"); log.debug("No keyboard");
@@ -112,40 +106,4 @@ public class BotResponseImpl implements BotResponse {
return update.message().messageId(); return update.message().messageId();
} }
private void logKeyboard(KeyboardButton[][] keyboard) {
if (log.isDebugEnabled()) {
for (int i = 0; i < keyboard.length; i++) {
if (keyboard[i].length > 0) {
String row = Stream.of(keyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
log.debug("Keyboard: {}", row);
}
}
}
}
private void logKeyboard(InlineKeyboardButton[][] keyboard) {
if (log.isDebugEnabled()) {
for (int i = 0; i < keyboard.length; i++) {
if (keyboard[i].length > 0) {
String row = Stream.of(keyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
log.debug("Inline keyboard: {}", row);
}
}
}
}
@SneakyThrows
private String getText(KeyboardButton btn) {
Field text = KeyboardButton.class.getDeclaredField("text");
ReflectionAccessor.getInstance().makeAccessible(text);
return String.valueOf(text.get(btn));
}
@SneakyThrows
private String getText(InlineKeyboardButton btn) {
Field text = InlineKeyboardButton.class.getDeclaredField("text");
ReflectionAccessor.getInstance().makeAccessible(text);
return String.valueOf(text.get(btn));
}
} }

View File

@@ -1,11 +1,17 @@
package ru.penkrat.stbf.impl.pengrad; package ru.penkrat.stbf.impl.pengrad;
import com.google.gson.internal.reflect.ReflectionAccessor;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.model.request.KeyboardButton; import com.pengrad.telegrambot.model.request.KeyboardButton;
import lombok.Value; import lombok.Value;
import ru.penkrat.stbf.api.Keyboard; import ru.penkrat.stbf.api.Keyboard;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Value @Value
class KeyboardImpl implements Keyboard { class KeyboardImpl implements Keyboard {
@@ -13,4 +19,50 @@ class KeyboardImpl implements Keyboard {
private InlineKeyboardButton[][] inlineKeyboard; private InlineKeyboardButton[][] inlineKeyboard;
private Map<Object, String> text = new HashMap<>();
@Override
public String toString() {
int k = keyboard != null ? keyboard.length : 0;
int kn = k > 0 ? keyboard[0].length : 0;
int i = inlineKeyboard != null ? inlineKeyboard.length : 0;
int in = i > 0 ? inlineKeyboard[0].length : 0;
return "KeyboardImpl(pengrad) [keyboard=" + k + "x" + kn
+ ", inline=" + i + "x" + in + "]";
}
String toFriendlyString() {
StringBuilder sb = new StringBuilder();
if (keyboard != null) {
sb.append("Keyboard:\n");
for (int i = 0; i < keyboard.length; i++) {
if (keyboard[i].length > 0) {
String row = Stream.of(keyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
sb.append(row).append("\n");
}
}
}
if (inlineKeyboard != null) {
sb.append("Inline keyboard:\n");
for (int i = 0; i < inlineKeyboard.length; i++) {
if (inlineKeyboard[i].length > 0) {
String row = Stream.of(inlineKeyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
sb.append(row).append("\n");
}
}
}
return sb.toString();
}
private String getText(Object btn) {
return text.computeIfAbsent(btn, o -> {
try {
Field text = btn.getClass().getDeclaredField("text");
ReflectionAccessor.getInstance().makeAccessible(text);
return String.valueOf(text.get(btn));
} catch (NoSuchFieldException | IllegalAccessException e) {
return "*error*";
}
});
}
} }

View File

@@ -13,6 +13,8 @@ public class PengradKeyboardBuilder implements KeyboardBuilder {
private InlineKeyboardButton[][] inlineKeyboard; private InlineKeyboardButton[][] inlineKeyboard;
private String keyboardStr = "";
public static Keyboard singleKey(Action action) { public static Keyboard singleKey(Action action) {
return KeyboardBuilder.newKeyboard().add(action).build(); return KeyboardBuilder.newKeyboard().add(action).build();
} }
@@ -123,7 +125,6 @@ public class PengradKeyboardBuilder implements KeyboardBuilder {
n[k.length] = btn; n[k.length] = btn;
keyboard[keyboard.length - 1] = n; keyboard[keyboard.length - 1] = n;
} }
} }
private void put(InlineKeyboardButton btn) { private void put(InlineKeyboardButton btn) {

View File

@@ -0,0 +1,8 @@
package ru.penkrat.stbf.templates;
import ru.penkrat.stbf.api.Action;
public interface ActionResolver {
Action getAction(String name);
}

View File

@@ -4,36 +4,48 @@ import lombok.experimental.UtilityClass;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Optional;
import java.util.function.Function;
@UtilityClass @UtilityClass
public class ReflectionUtils { public class ReflectionUtils {
public <T> T getMethodResult(Object context, String methodName, Class<T> clazz) { public <T> T getMethodResult(Object context, String methodName, Class<T> clazz) {
return getMethod(context.getClass(), methodName)
.map(invoker(context, clazz))
.orElse(null);
}
private Optional<Method> getMethod(Class<?> clazz, String methodName) {
try { try {
Method method = context.getClass().getMethod(methodName); Method method = clazz.getMethod(methodName);
method.setAccessible(true); method.setAccessible(true);
Object result = method.invoke(context); return Optional.of(method);
if (result != null && clazz.isAssignableFrom(result.getClass())) { } catch (NoSuchMethodException e) {
// try find as private
}
try {
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
return Optional.of(method);
} catch (NoSuchMethodException e) {
return Optional.empty();
}
}
private <T> Function<Method, T> invoker(Object target, Class<T> resultClass) {
return (method) -> {
Object result = null;
try {
result = method.invoke(target);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
if (result != null && resultClass.isAssignableFrom(result.getClass())) {
return (T) result; return (T) result;
} }
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null; return null;
};
} }
} }

View File

@@ -0,0 +1,15 @@
package ru.penkrat.stbf.templates.utils;
import lombok.experimental.UtilityClass;
@UtilityClass
public class StringUtils {
public boolean isEmpty(String string) {
return string == null || string.isEmpty();
}
public boolean isNotEmpty(String string) {
return !isEmpty(string);
}
}

View File

@@ -0,0 +1,29 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
class ActionItem extends NamedItem {
@JacksonXmlText
private String text;
@JacksonXmlProperty(isAttribute = true)
private String command;
@JacksonXmlProperty(isAttribute = true)
private boolean requestContact;
@JacksonXmlProperty(isAttribute = true)
private boolean requestLocation;
@JacksonXmlProperty(isAttribute = true)
private String callbackData;
@JacksonXmlProperty(isAttribute = true)
private String url;
}

View File

@@ -1,10 +1,8 @@
package ru.penkrat.stbf.templates.xml; package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -13,7 +11,10 @@ import lombok.Setter;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@JacksonXmlRootElement(localName = "button") @JacksonXmlRootElement(localName = "button")
class Button { class Button extends NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String actionRef;
@JacksonXmlText @JacksonXmlText
private String text; private String text;

View File

@@ -0,0 +1,46 @@
package ru.penkrat.stbf.templates.xml;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.templates.ActionResolver;
import ru.penkrat.stbf.templates.utils.StringUtils;
class FlowActionResolverDelegate implements ActionResolver {
private final NamedItemResolver<ActionItem> resolver;
FlowActionResolverDelegate(FlowRoot src) {
resolver = new NamedItemResolver<>(Traversal.traverse(src, FlowRoot::getActions));
}
@Override
public Action getAction(String key) {
final ActionItem actionItem = resolver.get(key);
boolean isInline = StringUtils.isNotEmpty(actionItem.getCallbackData())
&& StringUtils.isNotEmpty(actionItem.getUrl());
if (isInline) {
if (StringUtils.isNotEmpty(actionItem.getCommand())) {
throw new IllegalArgumentException("'command' is not allowed for inline button");
}
if (actionItem.isRequestContact()) {
throw new IllegalArgumentException("'requestContact' is not allowed for inline button");
}
if (actionItem.isRequestLocation()) {
throw new IllegalArgumentException("'requestLocation' is not allowed for inline button");
}
}
return Action.builder()
.text(actionItem.getText())
// btn only
.cmd(actionItem.getCommand())
.requestContact(actionItem.isRequestContact())
.requestLocation(actionItem.isRequestLocation())
// inline btn only
.inline(isInline)
.callbackData(actionItem.getCallbackData())
.url(actionItem.getUrl())
.build();
}
}

View File

@@ -0,0 +1,68 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
class FlowParser {
private final XmlMapper mapper = new XmlMapper();
private String basePath = "";
public FlowRoot read(File file) throws IOException {
return read(new FileInputStream(file));
}
public FlowRoot read(@NonNull String fileName) {
if (fileName.startsWith("classpath:")) {
String fn = fileName.replaceFirst("^classpath:", "");
basePath = "classpath:" + Paths.get(fileName).getParent().toString();
return read(FlowParser.class.getResourceAsStream(fn));
} else {
basePath = Paths.get(fileName).getParent().toString();
try {
return read(new FileInputStream(fileName));
} catch (FileNotFoundException e) {
throw new RuntimeException("Can't load file " + fileName, e);
}
}
}
public FlowRoot read(InputStream is) {
FlowRoot flow = readFlow(is);
includes(flow);
return flow;
}
private void includes(FlowRoot src) {
final List<FlowRoot> flowRoots = src.getIncludes().stream()
.map(IncludeItem::getFile)
.map(fileName -> readFlow(basePath, fileName))
.filter(Objects::nonNull)
.peek(this::includes)
.collect(Collectors.toList());
src.getIncluded().addAll(flowRoots);
}
private FlowRoot readFlow(InputStream is) {
try {
return mapper.readValue(is, FlowRoot.class);
} catch (IOException e) {
throw new RuntimeException("Can't load resource", e);
}
}
private FlowRoot readFlow(String basePath, String file) {
return readFlow(FlowParser.class.getResourceAsStream(file));
}
}

View File

@@ -0,0 +1,7 @@
package ru.penkrat.stbf.templates.xml;
import ru.penkrat.stbf.templates.ActionResolver;
import ru.penkrat.stbf.templates.ScreenResolver;
public interface FlowResolver extends ScreenResolver, ActionResolver {
}

View File

@@ -0,0 +1,31 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@JacksonXmlRootElement(localName = "flow")
class FlowRoot {
@Getter
@JacksonXmlElementWrapper(localName = "actions")
@JsonProperty("action")
private List<ActionItem> actions = new ArrayList<>();
@Getter
@JacksonXmlElementWrapper(localName = "screens")
@JsonProperty("screen")
private List<ScreenItem> screens = new ArrayList<>();
@Getter
@JacksonXmlElementWrapper(useWrapping = false)
@JsonProperty("include")
private List<IncludeItem> includes = new ArrayList<>();
@Getter
private transient List<FlowRoot> included = new ArrayList<>();
}

View File

@@ -1,6 +1,5 @@
package ru.penkrat.stbf.templates.xml; package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import lombok.val; import lombok.val;
@@ -9,46 +8,41 @@ import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder; import ru.penkrat.stbf.api.KeyboardBuilder;
import ru.penkrat.stbf.api.Screen; import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.common.screen.TextScreen; import ru.penkrat.stbf.common.screen.TextScreen;
import ru.penkrat.stbf.templates.ActionResolver;
import ru.penkrat.stbf.templates.KeyboardProvider;
import ru.penkrat.stbf.templates.ScreenResolver; import ru.penkrat.stbf.templates.ScreenResolver;
import ru.penkrat.stbf.templates.TemplateRenderer; import ru.penkrat.stbf.templates.TemplateRenderer;
import ru.penkrat.stbf.templates.utils.ReflectionUtils; import ru.penkrat.stbf.templates.utils.ReflectionUtils;
import ru.penkrat.stbf.templates.utils.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
public class XmlScreenResolver implements ScreenResolver { class FlowScreenResolverDelegate implements ScreenResolver {
private final XmlMapper mapper = new XmlMapper(); private final NamedItemResolver<ScreenItem> resolver;
private final Map<String, List<ScreenItem>> byId; private final ActionResolver actionResolver;
private final Map<String, List<ScreenItem>> byName;
@Setter @Setter
private TemplateRenderer templateRenderer = new NoopTemplateRenderer(); private TemplateRenderer templateRenderer = new NoopTemplateRenderer();
public XmlScreenResolver(InputStream is) throws IOException { FlowScreenResolverDelegate(FlowRoot src, ActionResolver actionResolver) {
Screens screens = mapper.readValue(is, Screens.class); this.actionResolver = actionResolver;
resolver = new NamedItemResolver<>(Traversal.traverse(src, FlowRoot::getScreens));
byId = screens.getScreens().stream()
.collect(Collectors.groupingBy(ScreenItem::getId));
byName = screens.getScreens().stream()
.collect(Collectors.groupingBy(ScreenItem::getName));
} }
@Override @Override
public Screen getScreen(String name) { public Screen getScreen(String name) {
ScreenItem item = get(name); ScreenItem item = resolver.get(name);
return new TextScreen(item.getText(), buildKeyboard(item.getKeyboard(), null)); return new TextScreen(item.getText(), buildKeyboard(item.getKeyboard(), null));
} }
@Override @Override
public Screen getScreen(String name, Object context) { public Screen getScreen(String name, Object context) {
ScreenItem item = get(name); ScreenItem item = resolver.get(name);
return new TextScreen(templateRenderer.render(item.getText(), context), return new TextScreen(templateRenderer.render(item.getText(), context),
resolveKeyboard(item.getKeyboard(), context)); resolveKeyboard(item.getKeyboard(), context));
@@ -58,6 +52,13 @@ public class XmlScreenResolver implements ScreenResolver {
if (wrapper == null) if (wrapper == null)
return null; return null;
if (context instanceof KeyboardProvider) {
final Keyboard keyboard = ((KeyboardProvider) context).getKeyboard();
if (keyboard == null) {
log.warn("Method 'getKeyboard' returns NULL value!", wrapper.getFactoryMethod());
}
return keyboard;
}
if (wrapper.getFactoryMethod() != null && !wrapper.getFactoryMethod().isEmpty() && context != null) { if (wrapper.getFactoryMethod() != null && !wrapper.getFactoryMethod().isEmpty() && context != null) {
val keyboard = ReflectionUtils.getMethodResult(context, wrapper.getFactoryMethod(), Keyboard.class); val keyboard = ReflectionUtils.getMethodResult(context, wrapper.getFactoryMethod(), Keyboard.class);
if (keyboard == null) { if (keyboard == null) {
@@ -77,11 +78,7 @@ public class XmlScreenResolver implements ScreenResolver {
for (ButtonsRow row : wrapper.getRows()) { for (ButtonsRow row : wrapper.getRows()) {
List<Action> buttons = row.getButtons().stream() List<Action> buttons = row.getButtons().stream()
.filter(btn -> checkIfCondition(context, btn.getIfCondition())) .filter(btn -> checkIfCondition(context, btn.getIfCondition()))
.map(btn -> Action.builder() .map(btn -> getAction(btn))
.text(btn.getText())
.requestContact(btn.isRequestContact())
.requestLocation(btn.isRequestLocation())
.build())
.collect(Collectors.toList()); .collect(Collectors.toList());
builder.row(buttons.toArray(new Action[buttons.size()])); builder.row(buttons.toArray(new Action[buttons.size()]));
@@ -92,6 +89,17 @@ public class XmlScreenResolver implements ScreenResolver {
return keyboard; return keyboard;
} }
private Action getAction(Button btn) {
if (StringUtils.isNotEmpty(btn.getActionRef())) {
return actionResolver.getAction(btn.getActionRef());
}
return Action.builder()
.text(btn.getText())
.requestContact(btn.isRequestContact())
.requestLocation(btn.isRequestLocation())
.build();
}
private boolean checkIfCondition(Object context, String ifCondition) { private boolean checkIfCondition(Object context, String ifCondition) {
if (ifCondition == null || ifCondition.isEmpty()) { if (ifCondition == null || ifCondition.isEmpty()) {
return true; return true;
@@ -109,22 +117,4 @@ public class XmlScreenResolver implements ScreenResolver {
return ReflectionUtils.getMethodResult(context, ifCondition, Boolean.class); return ReflectionUtils.getMethodResult(context, ifCondition, Boolean.class);
} }
private ScreenItem get(String key) {
List<ScreenItem> list = byId.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'id' " + key);
}
return list.get(0);
}
list = byName.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'name' " + key);
}
return list.get(0);
}
throw new IllegalStateException("Screen not found by 'id' or 'name' " + key);
}
} }

View File

@@ -0,0 +1,8 @@
package ru.penkrat.stbf.templates.xml;
import lombok.Data;
@Data
class IncludeItem {
private String file;
}

View File

@@ -0,0 +1,19 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
abstract class NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String id;
@JacksonXmlProperty(isAttribute = true)
private String name;
@JacksonXmlProperty(isAttribute = true)
private String ref;
}

View File

@@ -0,0 +1,42 @@
package ru.penkrat.stbf.templates.xml;
import ru.penkrat.stbf.templates.utils.StringUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class NamedItemResolver<T extends NamedItem> {
private final Map<String, List<T>> byId;
private final Map<String, List<T>> byName;
NamedItemResolver(Collection<T> src) {
byId = src.stream()
.filter(item -> StringUtils.isNotEmpty(item.getId()))
.collect(Collectors.groupingBy(NamedItem::getId));
byName = src.stream()
.filter(item -> StringUtils.isNotEmpty(item.getName()))
.collect(Collectors.groupingBy(NamedItem::getName));
}
public T get(String key) {
List<T> list = byId.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'id' " + key);
}
return list.get(0);
}
list = byName.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'name' " + key);
}
return list.get(0);
}
throw new IllegalStateException("Element not found by 'id' or 'name' " + key);
}
}

View File

@@ -1,19 +1,11 @@
package ru.penkrat.stbf.templates.xml; package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@Getter @Getter
@Setter @Setter
class ScreenItem { class ScreenItem extends NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String id;
@JacksonXmlProperty(isAttribute = true)
private String name;
private String text; private String text;

View File

@@ -1,20 +0,0 @@
package ru.penkrat.stbf.templates.xml;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.Getter;
@JacksonXmlRootElement(localName = "screens")
class Screens {
@Getter
@JacksonXmlElementWrapper(useWrapping = false, localName = "screen")
@JsonProperty("screen")
private List<ScreenItem> screens = new ArrayList<>();
}

View File

@@ -0,0 +1,24 @@
package ru.penkrat.stbf.templates.xml;
import lombok.experimental.UtilityClass;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Function;
@UtilityClass
class Traversal {
public <T> Collection<T> traverse(FlowRoot flow, Function<FlowRoot, Collection<T>> getter) {
final ArrayList<T> target = new ArrayList<>();
traverse(flow, getter, target);
return target;
}
private <T> void traverse(FlowRoot flow, Function<FlowRoot, Collection<T>> getter, Collection<T> target) {
Collection<T> c = getter.apply(flow);
target.addAll(c);
flow.getIncluded().forEach(f -> traverse(f, getter, target));
}
}

View File

@@ -0,0 +1,50 @@
package ru.penkrat.stbf.templates.xml;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.templates.TemplateRenderer;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@Slf4j
public class XmlFlowResolver implements FlowResolver {
private final FlowParser reader = new FlowParser();
private final FlowActionResolverDelegate actionDelegate;
private final FlowScreenResolverDelegate screenDelegate;
public XmlFlowResolver(String filename) {
FlowRoot flow = reader.read(filename);
actionDelegate = new FlowActionResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this);
}
public XmlFlowResolver(InputStream is) {
FlowRoot flow = reader.read(is);
actionDelegate = new FlowActionResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this);
}
@Override
public Screen getScreen(String name) {
return screenDelegate.getScreen(name);
}
@Override
public Screen getScreen(String name, Object context) {
return screenDelegate.getScreen(name, context);
}
@Override
public Action getAction(String name) {
return actionDelegate.getAction(name);
}
public void setTemplateRenderer(TemplateRenderer templateRenderer) {
screenDelegate.setTemplateRenderer(templateRenderer);
}
}

View File

@@ -0,0 +1,112 @@
package ru.penkrat.stbf.templates.xml;
import org.junit.Before;
import org.junit.Test;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.templates.impl.MustacheRenderer;
import ru.penkrat.stbf.templates.utils.ReflectionUtils;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
public class XmlFlowResolverTest {
@Before
public void setUp() throws Exception {
}
@Test
public void testRead() throws Exception {
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("screens.xml");
XmlFlowResolver resolver = new XmlFlowResolver(xmlUnderTest);
Screen screen1 = resolver.getScreen("screen-1");
assertThat(screen1).isNotNull();
assertThat(screen1.getText()).isEqualTo("Test text");
assertThat(screen1.getKeyboard()).isNotNull();
String keyboard = ReflectionUtils.getMethodResult(screen1.getKeyboard(), "toFriendlyString", String.class);
assertThat(keyboard).contains("Send phone");
}
@Test
public void testReadWithTemplates() throws Exception {
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("screens.xml");
XmlFlowResolver resolver = new XmlFlowResolver(xmlUnderTest);
resolver.setTemplateRenderer(new MustacheRenderer());
Keyboard keyboard = KeyboardBuilder.newKeyboard().build();
Screen screen2 = resolver.getScreen("screen-2", new Object() {
@SuppressWarnings("unused")
public String name = "Tester";
@SuppressWarnings("unused")
public Keyboard getKeyboard() {
return keyboard;
}
});
assertThat(screen2).isNotNull();
assertThat(screen2.getText()).isEqualTo("Hello, Tester");
assertThat(screen2.getKeyboard()).isEqualTo(keyboard);
}
@Test
public void testReadWithInclude() throws Exception {
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("flow.xml");
XmlFlowResolver resolver = new XmlFlowResolver(xmlUnderTest);
assertThat(resolver.getScreen("screen-1")).isNotNull();
}
@Test
public void testReadWithIncludeWithInclude() throws Exception {
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("flow.xml");
XmlFlowResolver resolver = new XmlFlowResolver(xmlUnderTest);
assertThat(resolver.getAction("4001")).isNotNull();
}
@Test
public void testReadFile() throws Exception {
Path tmp = Files.createTempFile("test-flow", ".xml");
OutputStream fos = new FileOutputStream(tmp.toFile());
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("flow.xml");
int b = xmlUnderTest.read();
while (b >= 0) {
fos.write(b);
b = xmlUnderTest.read();
}
fos.close();
XmlFlowResolver resolver = new XmlFlowResolver(tmp.toFile().getAbsolutePath());
assertThat(resolver.getScreen("screen-1")).isNotNull();
assertThat(resolver.getAction("3001")).isNotNull();
Files.deleteIfExists(tmp);
}
@Test
public void testReadClasspath() throws Exception {
XmlFlowResolver resolver = new XmlFlowResolver("classpath:/ru/penkrat/stbf/templates/xml/flow.xml");
assertThat(resolver.getScreen("screen-1")).isNotNull();
assertThat(resolver.getAction("3001")).isNotNull();
}
}

View File

@@ -1,59 +0,0 @@
package ru.penkrat.stbf.templates.xml;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.InputStream;
import org.junit.Before;
import org.junit.Test;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.templates.impl.MustacheRenderer;
public class XmlScreenResolverTest {
@Before
public void setUp() throws Exception {
}
@Test
public void testRead() throws Exception {
InputStream xmlUnderTest = XmlScreenResolverTest.class.getResourceAsStream("screens.xml");
XmlScreenResolver resolver = new XmlScreenResolver(xmlUnderTest);
Screen screen1 = resolver.getScreen("screen-1");
assertThat(screen1).isNotNull();
assertThat(screen1.getText()).isEqualTo("Test text");
assertThat(screen1.getKeyboard()).isNotNull();
}
@Test
public void testReadWithTemplates() throws Exception {
InputStream xmlUnderTest = XmlScreenResolverTest.class.getResourceAsStream("screens.xml");
XmlScreenResolver resolver = new XmlScreenResolver(xmlUnderTest);
resolver.setTemplateRenderer(new MustacheRenderer());
Keyboard keyboard = KeyboardBuilder.newKeyboard().build();
Screen screen2 = resolver.getScreen("screen-2", new Object() {
@SuppressWarnings("unused")
public String name = "Tester";
@SuppressWarnings("unused")
public Keyboard getKeyboard() {
return keyboard;
}
});
assertThat(screen2).isNotNull();
assertThat(screen2.getText()).isEqualTo("Hello, Tester");
assertThat(screen2.getKeyboard()).isEqualTo(keyboard);
}
}

View File

@@ -6,11 +6,6 @@ import org.junit.Test;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import ru.penkrat.stbf.templates.xml.Button;
import ru.penkrat.stbf.templates.xml.ButtonsRow;
import ru.penkrat.stbf.templates.xml.ScreenItem;
import ru.penkrat.stbf.templates.xml.Screens;
public class XmlWriterTest { public class XmlWriterTest {
private final XmlMapper mapper = new XmlMapper(); private final XmlMapper mapper = new XmlMapper();
@@ -18,11 +13,12 @@ public class XmlWriterTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
mapper.enable(SerializationFeature.INDENT_OUTPUT); mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
} }
@Test @Test
public void testWrite() throws Exception { public void testWrite() throws Exception {
Screens root = new Screens(); FlowRoot root = new FlowRoot();
ScreenItem item1 = new ScreenItem(); ScreenItem item1 = new ScreenItem();
item1.setId("1"); item1.setId("1");
@@ -52,6 +48,10 @@ public class XmlWriterTest {
root.getScreens().add(item1); root.getScreens().add(item1);
root.getScreens().add(item2); root.getScreens().add(item2);
root.getActions().add(new ActionItem());
root.getIncludes().add(new IncludeItem());
String xml = mapper.writeValueAsString(root); String xml = mapper.writeValueAsString(root);
System.out.println(xml); System.out.println(xml);

View File

@@ -0,0 +1,20 @@
<flow>
<include file="screens.xml"/>
<include file="/test/a.xml"/>
<actions>
<action id="2001" requestContact="true">Send phone</action>
</actions>
<screens>
<screen id="5001" name="screen-5001">
<text>Test text</text>
<keyboard>
<row>
<button>Btn1</button>
<button>Btn2</button>
</row>
</keyboard>
</screen>
</screens>
</flow>

View File

@@ -1,3 +1,7 @@
<flow>
<actions>
<action id="1001" requestContact="true">Send phone</action>
</actions>
<screens> <screens>
<screen id="1" name="screen-1"> <screen id="1" name="screen-1">
<text>Test text</text> <text>Test text</text>
@@ -5,6 +9,7 @@
<row> <row>
<button if="false">Btn1</button> <button if="false">Btn1</button>
<button>Btn2</button> <button>Btn2</button>
<button actionRef="1001">Action.name</button>
</row> </row>
<row> <row>
<button>Btn1</button> <button>Btn1</button>
@@ -17,3 +22,4 @@
<keyboard factoryMethod="getKeyboard"/> <keyboard factoryMethod="getKeyboard"/>
</screen> </screen>
</screens> </screens>
</flow>

View File

@@ -0,0 +1,6 @@
<flow>
<include file="/test/b/b.xml"/>
<actions>
<action id="3001">A action</action>
</actions>
</flow>

View File

@@ -0,0 +1,5 @@
<flow>
<actions>
<action id="4001">B action</action>
</actions>
</flow>