Skip to content

Commit eeeb2ec

Browse files
mvysnyjavier-godoy
authored andcommitted
feat: implement StateMemoizer which will help us to remember config set via ITerminalOptions
Close #14
1 parent f356a92 commit eeeb2ec

File tree

3 files changed

+207
-3
lines changed

3 files changed

+207
-3
lines changed

src/main/java/com/flowingcode/vaadin/addons/xterm/PreserveStateAddon.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
package com.flowingcode.vaadin.addons.xterm;
22

3+
import com.flowingcode.vaadin.addons.xterm.utils.StateMemoizer;
4+
import lombok.experimental.Delegate;
5+
36
import java.util.Objects;
47
import java.util.concurrent.CompletableFuture;
58

69
/**
710
* Add-on which preserves the client-side state when the component is removed
8-
* from the UI then reattached later on.
11+
* from the UI then reattached later on. The problem here is that when the
12+
* {@link XTerm} server-side component is detached from the UI, the xterm.js client-side
13+
* component is destroyed along with its state. When the {@link XTerm} component
14+
* is later re-attached to the UI, a new unconfigured xterm.js is created on the
15+
* client-side.
916
* <p></p>
1017
* To use this addon, simply create the addon then make sure to call all {@link ITerminal}
1118
* and {@link ITerminalOptions} methods via this addon:
@@ -16,19 +23,49 @@
1623
* addon.write("$ ");
1724
* </pre>
1825
*/
19-
public class PreserveStateAddon implements ITerminal {
20-
private final XTermBase xterm;
26+
public class PreserveStateAddon implements ITerminal, ITerminalOptions {
27+
/**
28+
* The xterm to delegate all calls to.
29+
*/
30+
private final XTerm xterm;
31+
/**
32+
* Remembers everything that was printed into the xterm and what the user typed in.
33+
*/
2134
private final StringBuilder scrollbackBuffer = new StringBuilder();
2235
/**
2336
* All commands are properly applied before the first attach; they're just
2437
* not preserved after subsequent detach/attach.
2538
*/
2639
private boolean wasDetachedOnce = false;
40+
/**
41+
* Used to re-apply all options to the xterm after it has been reattached back to the UI.
42+
* Otherwise, the options would not be applied to the client-side xterm.js component.
43+
*/
44+
private final StateMemoizer optionsMemoizer;
45+
46+
/**
47+
* Delegate all option setters through this delegate, which is the {@link #optionsMemoizer} proxy.
48+
* That will allow us to re-apply the settings when the xterm is re-attached.
49+
* <p></p>
50+
* For example, calling {@link ITerminalOptions#setBellSound(String)}
51+
* on this addon will pass through the call to this delegate, which in turn passes
52+
* the call to {@link #optionsMemoizer} which remembers the call and passes
53+
* it to {@link #xterm}.
54+
* <p></p>
55+
* After the xterm.js is re-attached, we simply call {@link StateMemoizer#apply()}
56+
* to apply all changed setters again to xterm.js, to make sure xterm.js is
57+
* configured.
58+
*/
59+
@Delegate
60+
private final ITerminalOptions optionsDelegate;
2761

2862
public PreserveStateAddon(XTerm xterm) {
2963
this.xterm = Objects.requireNonNull(xterm);
64+
optionsMemoizer = new StateMemoizer(xterm, ITerminalOptions.class);
65+
optionsDelegate = (ITerminalOptions) optionsMemoizer.getProxy();
3066
xterm.addAttachListener(e -> {
3167
if (wasDetachedOnce) {
68+
optionsMemoizer.apply();
3269
xterm.write(scrollbackBuffer.toString());
3370
}
3471
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.flowingcode.vaadin.addons.xterm.utils;
2+
3+
import java.io.Serializable;
4+
import java.lang.reflect.InvocationTargetException;
5+
import java.lang.reflect.Method;
6+
import java.lang.reflect.Proxy;
7+
import java.util.Arrays;
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
import java.util.Objects;
11+
12+
/**
13+
* Remembers the values passed to all setters. At any time you can reapply
14+
* those calls by using {@link #apply()}.
15+
*/
16+
public class StateMemoizer implements Serializable {
17+
/**
18+
* Remember all calls to all setters; also remember what args were passed to those setters.
19+
*/
20+
private final Map<String, Serializable> setterCalls = new HashMap<>();
21+
/**
22+
* Pass-through the setters here.
23+
*/
24+
private final Object delegate;
25+
/**
26+
* Setters invoked on this proxy will have the args remembered; the methods invocations
27+
* will then be passed on to {@link #delegate}.
28+
*/
29+
private final Object proxy;
30+
31+
/**
32+
* Creates the memoizer. Remember to invoke interface methods via {@link #getProxy()} in
33+
* order for this to work.
34+
* @param delegate pass-through the setters here.
35+
* @param interfaces used to create the memoizing proxy.
36+
*/
37+
public StateMemoizer(Object delegate, Class<?>... interfaces) {
38+
this.delegate = Objects.requireNonNull(delegate);
39+
proxy = Proxy.newProxyInstance(interfaces[0].getClassLoader(), interfaces, (proxy, method, args) -> {
40+
if (method.getName().startsWith("set") && args.length == 1) {
41+
// remember the state
42+
setterCalls.put(method.getName(), (Serializable) args[0]);
43+
}
44+
return method.invoke(delegate, args);
45+
});
46+
}
47+
48+
/**
49+
* Setters invoked on this proxy will have the args remembered; the methods invocations
50+
* will then be passed on to {@link #delegate}.
51+
* @return the proxy, not null.
52+
*/
53+
public Object getProxy() {
54+
return proxy;
55+
}
56+
57+
/**
58+
* Calls all setters again on {@link #delegate}.
59+
*/
60+
public void apply() {
61+
setterCalls.forEach((k, v) -> {
62+
final Method method = Arrays.stream(delegate.getClass().getMethods()).filter(it -> it.getName().equals(k)).findAny().get();
63+
try {
64+
method.invoke(delegate, v);
65+
} catch (IllegalAccessException | InvocationTargetException e) {
66+
throw new RuntimeException(e);
67+
}
68+
});
69+
}
70+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.flowingcode.vaadin.addons.xterm.utils;
2+
3+
import org.junit.Before;
4+
import org.junit.Test;
5+
6+
import static org.junit.Assert.assertEquals;
7+
import static org.junit.Assert.assertNull;
8+
9+
public class StateMemoizerTest {
10+
11+
private StateMemoizer memoizer;
12+
private BunchOfSetters proxy;
13+
private MyBean bean;
14+
15+
public static interface BunchOfSetters {
16+
void setFoo(String value);
17+
void setBar(int value);
18+
}
19+
20+
public static class MyBean implements BunchOfSetters {
21+
private String foo;
22+
private int bar;
23+
24+
public String getFoo() {
25+
return foo;
26+
}
27+
28+
public void setFoo(String foo) {
29+
this.foo = foo;
30+
}
31+
32+
public int getBar() {
33+
return bar;
34+
}
35+
36+
public void setBar(int bar) {
37+
this.bar = bar;
38+
}
39+
}
40+
41+
@Before
42+
public void setupTestValues() {
43+
bean = new MyBean();
44+
memoizer = new StateMemoizer(bean, BunchOfSetters.class);
45+
proxy = (BunchOfSetters) memoizer.getProxy();
46+
}
47+
48+
@Test
49+
public void emptyMemoizerApplySucceedsButDoesNothing() {
50+
memoizer.apply();
51+
assertNull(bean.getFoo());
52+
assertEquals(0, bean.getBar());
53+
}
54+
55+
@Test
56+
public void proxyModificationPassesValuesThrough() {
57+
proxy.setFoo("foo");
58+
assertEquals("foo", bean.getFoo());
59+
proxy.setBar(25);
60+
assertEquals(25, bean.getBar());
61+
}
62+
63+
@Test
64+
public void applyAppliesInvokedSettersOnly() {
65+
proxy.setFoo("foo");
66+
bean.setFoo("bar");
67+
bean.setBar(25);
68+
memoizer.apply();
69+
assertEquals("foo", bean.getFoo());
70+
assertEquals(25, bean.getBar());
71+
}
72+
73+
@Test
74+
public void applyBasicTest() {
75+
proxy.setFoo("foo");
76+
proxy.setBar(25);
77+
bean.setFoo("FOO");
78+
bean.setBar(26);
79+
memoizer.apply();
80+
assertEquals("foo", bean.getFoo());
81+
assertEquals(25, bean.getBar());
82+
}
83+
84+
@Test
85+
public void consequentSetterCallsAppliedProperly() {
86+
proxy.setFoo("foo");
87+
assertEquals("foo", bean.getFoo());
88+
proxy.setFoo("bar");
89+
assertEquals("bar", bean.getFoo());
90+
proxy.setBar(25);
91+
bean.setFoo("FOO");
92+
bean.setBar(26);
93+
memoizer.apply();
94+
assertEquals("bar", bean.getFoo());
95+
assertEquals(25, bean.getBar());
96+
}
97+
}

0 commit comments

Comments
 (0)