add flow: actions + screens

This commit is contained in:
2021-08-30 23:34:24 +03:00
parent 81cac78737
commit c930070b9c
19 changed files with 428 additions and 194 deletions

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.Method;
import java.util.Optional;
import java.util.function.Function;
@UtilityClass
public class ReflectionUtils {
public <T> T getMethodResult(Object context, String methodName, Class<T> clazz) {
try {
Method method = context.getClass().getMethod(methodName);
method.setAccessible(true);
Object result = method.invoke(context);
if (result != null && clazz.isAssignableFrom(result.getClass())) {
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 getMethod(context.getClass(), methodName)
.map(invoker(context, clazz))
.orElse(null);
}
private Optional<Method> getMethod(Class<?> clazz, String methodName) {
try {
Method method = clazz.getMethod(methodName);
method.setAccessible(true);
return Optional.of(method);
} 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 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;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -13,7 +11,10 @@ import lombok.Setter;
@Setter
@NoArgsConstructor
@JacksonXmlRootElement(localName = "button")
class Button {
class Button extends NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String actionRef;
@JacksonXmlText
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<>(src.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,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;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
@@ -9,46 +8,41 @@ import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder;
import ru.penkrat.stbf.api.Screen;
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.TemplateRenderer;
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.Map;
import java.util.stream.Collectors;
@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 Map<String, List<ScreenItem>> byName;
private final ActionResolver actionResolver;
@Setter
private TemplateRenderer templateRenderer = new NoopTemplateRenderer();
public XmlScreenResolver(InputStream is) throws IOException {
Screens screens = mapper.readValue(is, Screens.class);
byId = screens.getScreens().stream()
.collect(Collectors.groupingBy(ScreenItem::getId));
byName = screens.getScreens().stream()
.collect(Collectors.groupingBy(ScreenItem::getName));
FlowScreenResolverDelegate(FlowRoot src, ActionResolver actionResolver) {
this.actionResolver = actionResolver;
resolver = new NamedItemResolver<>(src.getScreens());
}
@Override
public Screen getScreen(String name) {
ScreenItem item = get(name);
ScreenItem item = resolver.get(name);
return new TextScreen(item.getText(), buildKeyboard(item.getKeyboard(), null));
}
@Override
public Screen getScreen(String name, Object context) {
ScreenItem item = get(name);
ScreenItem item = resolver.get(name);
return new TextScreen(templateRenderer.render(item.getText(), context),
resolveKeyboard(item.getKeyboard(), context));
@@ -58,6 +52,13 @@ public class XmlScreenResolver implements ScreenResolver {
if (wrapper == 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) {
val keyboard = ReflectionUtils.getMethodResult(context, wrapper.getFactoryMethod(), Keyboard.class);
if (keyboard == null) {
@@ -77,11 +78,7 @@ public class XmlScreenResolver implements ScreenResolver {
for (ButtonsRow row : wrapper.getRows()) {
List<Action> buttons = row.getButtons().stream()
.filter(btn -> checkIfCondition(context, btn.getIfCondition()))
.map(btn -> Action.builder()
.text(btn.getText())
.requestContact(btn.isRequestContact())
.requestLocation(btn.isRequestLocation())
.build())
.map(btn -> getAction(btn))
.collect(Collectors.toList());
builder.row(buttons.toArray(new Action[buttons.size()]));
@@ -92,6 +89,17 @@ public class XmlScreenResolver implements ScreenResolver {
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) {
if (ifCondition == null || ifCondition.isEmpty()) {
return true;
@@ -109,22 +117,4 @@ public class XmlScreenResolver implements ScreenResolver {
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;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
class ScreenItem {
@JacksonXmlProperty(isAttribute = true)
private String id;
@JacksonXmlProperty(isAttribute = true)
private String name;
class ScreenItem extends NamedItem {
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,45 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
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.IOException;
import java.io.InputStream;
@Slf4j
public class XmlFlowResolver implements FlowResolver {
private final XmlMapper mapper = new XmlMapper();
private final FlowActionResolverDelegate actionDelegate;
private final FlowScreenResolverDelegate screenDelegate;
public XmlFlowResolver(InputStream is) throws IOException {
FlowRoot flow = mapper.readValue(is, FlowRoot.class);
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,62 @@
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.InputStream;
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);
}
}

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.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 {
private final XmlMapper mapper = new XmlMapper();
@@ -18,11 +13,12 @@ public class XmlWriterTest {
@Before
public void setUp() throws Exception {
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
}
@Test
public void testWrite() throws Exception {
Screens root = new Screens();
FlowRoot root = new FlowRoot();
ScreenItem item1 = new ScreenItem();
item1.setId("1");
@@ -52,6 +48,10 @@ public class XmlWriterTest {
root.getScreens().add(item1);
root.getScreens().add(item2);
root.getActions().add(new ActionItem());
root.getIncludes().add(new IncludeItem());
String xml = mapper.writeValueAsString(root);
System.out.println(xml);

View File

@@ -1,10 +1,15 @@
<screens>
<flow>
<actions>
<action id="1001" requestContact="true">Send phone</action>
</actions>
<screens>
<screen id="1" name="screen-1">
<text>Test text</text>
<keyboard>
<row>
<button if="false">Btn1</button>
<button>Btn2</button>
<button actionRef="1001">Action.name</button>
</row>
<row>
<button>Btn1</button>
@@ -16,4 +21,5 @@
<text>Hello, {{ name }}</text>
<keyboard factoryMethod="getKeyboard"/>
</screen>
</screens>
</screens>
</flow>