StringInput masking

- Add a feature to define a mask character for StringInput
  which help when there's a need to as something sensitive.
- This masks both input and result value.
This commit is contained in:
Janne Valkealahti
2022-02-08 10:26:17 +00:00
parent e3604a5bc5
commit 2f4209785e
4 changed files with 132 additions and 10 deletions

View File

@@ -40,6 +40,7 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
private final String defaultValue;
private StringInputContext currentContext;
private Character maskCharacter;
public StringInput(Terminal terminal) {
this(terminal, null, null, null);
@@ -57,12 +58,21 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
this.defaultValue = defaultValue;
}
/**
* Sets a mask character for input and result value.
*
* @param maskCharacter a mask character
*/
public void setMaskCharater(Character maskCharacter) {
this.maskCharacter = maskCharacter;
}
@Override
protected StringInputContext getThisContext(ComponentContext<?> context) {
if (context != null && currentContext == context) {
return currentContext;
}
currentContext = StringInputContext.of(defaultValue);
currentContext = StringInputContext.of(defaultValue, maskCharacter);
currentContext.setName(getName());
context.stream().forEach(e -> {
currentContext.put(e.getKey(), e.getValue());
@@ -123,13 +133,48 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
*/
void setDefaultValue(String defaultValue);
/**
* Sets a mask character.
*
* @param maskCharacter the mask character
*/
void setMaskCharacter(Character maskCharacter);
/**
* Gets a masked input.
*
* @return a masked input
*/
String getMaskedInput();
/**
* Gets a masked result value.
*
* @return masked result value
*/
String getMaskedResultValue();
/**
* Returns flag if there is a mask character defined.
*
* @return true if mask character defined, false otherwise
*/
boolean hasMaskCharacter();
/**
* Gets a mask character.
*
* @return a mask character.
*/
Character getMaskCharacter();
/**
* Gets an empty {@link StringInputContext}.
*
* @return empty path input context
*/
public static StringInputContext empty() {
return of(null);
return of(null, null);
}
/**
@@ -137,8 +182,8 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
*
* @return path input context
*/
public static StringInputContext of(String defaultValue) {
return new DefaultStringInputContext(defaultValue);
public static StringInputContext of(String defaultValue, Character maskCharacter) {
return new DefaultStringInputContext(defaultValue, maskCharacter);
}
}
@@ -146,9 +191,11 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
implements StringInputContext {
private String defaultValue;
private Character maskCharacter;
public DefaultStringInputContext(String defaultValue) {
public DefaultStringInputContext(String defaultValue, Character maskCharacter) {
this.defaultValue = defaultValue;
this.maskCharacter = maskCharacter;
}
@Override
@@ -161,14 +208,52 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
this.defaultValue = defaultValue;
}
@Override
public void setMaskCharacter(Character maskCharacter) {
this.maskCharacter = maskCharacter;
}
@Override
public String getMaskedInput() {
return maybeMask(getInput());
}
@Override
public String getMaskedResultValue() {
return maybeMask(getResultValue());
}
@Override
public boolean hasMaskCharacter() {
return maskCharacter != null;
}
@Override
public Character getMaskCharacter() {
return maskCharacter;
}
@Override
public Map<String, Object> toTemplateModel() {
Map<String, Object> attributes = super.toTemplateModel();
attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null);
attributes.put("maskedInput", getMaskedInput());
attributes.put("maskedResultValue", getMaskedResultValue());
attributes.put("maskCharacter", getMaskCharacter());
attributes.put("hasMaskCharacter", hasMaskCharacter());
Map<String, Object> model = new HashMap<>();
model.put("model", attributes);
return model;
}
private String maybeMask(String str) {
if (StringUtils.hasLength(str) && maskCharacter != null) {
return new String(new char[str.length()]).replace('\0', maskCharacter);
}
else {
return str;
}
}
}
private class DefaultRenderer implements Function<StringInputContext, List<AttributedString>> {

View File

@@ -1,9 +1,17 @@
// info section after '? xxx'
info(model) ::= <%
<if(model.input)>
<model.input>
<if(model.hasMaskCharacter)>
<if(model.maskedInput)>
<model.maskedInput>
<else>
<("[Default "); format="value"><model.defaultValue; format="value"><("]"); format="value">
<endif>
<else>
<("[Default "); format="value"><model.defaultValue; format="value"><("]"); format="value">
<if(model.input)>
<model.input>
<else>
<("[Default "); format="value"><model.defaultValue; format="value"><("]"); format="value">
<endif>
<endif>
%>
@@ -14,7 +22,7 @@ question_name(model) ::= <<
// component result
result(model) ::= <<
<question_name(model)> <model.resultValue; format="value">
<question_name(model)> <model.maskedResultValue; format="value">
>>
// component is running

View File

@@ -85,6 +85,32 @@ public class StringInputTests extends AbstractShellTests {
assertThat(consoleOut()).contains("component1 component1ResultValue");
}
@Test
public void testResultBasicWithMask() throws InterruptedException {
ComponentContext<?> empty = ComponentContext.empty();
StringInput component1 = new StringInput(getTerminal(), "component1", "component1ResultValue");
component1.setPrintResults(true);
component1.setMaskCharater('*');
component1.setResourceLoader(new DefaultResourceLoader());
component1.setTemplateExecutor(getTemplateExecutor());
service.execute(() -> {
StringInputContext run1Context = component1.run(empty);
result1.set(run1Context);
latch1.countDown();
});
TestBuffer testBuffer = new TestBuffer().cr();
write(testBuffer.getBytes());
latch1.await(2, TimeUnit.SECONDS);
StringInputContext run1Context = result1.get();
assertThat(run1Context).isNotNull();
assertThat(run1Context.getResultValue()).isEqualTo("component1ResultValue");
assertThat(consoleOut()).contains("component1 *********************");
}
@Test
public void testResultUserInput() throws InterruptedException {
ComponentContext<?> empty = ComponentContext.empty();

View File

@@ -51,10 +51,13 @@ public class ComponentCommands extends AbstractShellComponent implements Resourc
}
@ShellMethod(key = "component string", value = "String input", group = "Components")
public String stringInput() {
public String stringInput(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue");
component.setResourceLoader(resourceLoader);
component.setTemplateExecutor(templateExecutor);
if (mask) {
component.setMaskCharater('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}