-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Welcome to the scenario wiki!
Each time when we write the CLI application on Java, we have to answer the following questions and then implement the corresponding logic:
- design the menu system:
- which menu should be displayed first?
- which items (choices) should each menu contain?
- which menu should be displayed when user chooses the action or types some text?
- how to prevent the user from typing incorrect value?
- how to call the needed business logic when some action is chosen?
- how to be sure that menu won't fall into never-ending cycle (user won't be able to exit the app)?
- how to separate the menu system from the other application parts, so it can be easily replaced with another UI if needed?
... and many others.
It leads to nested if or switch statements, input control classes and other boilerplate code.
The main purpose of the scenario library is to delegate all the CLI menu handling to it, so it will act as a service.
Add the following dependency to your project's pom.xml:
<dependency>
<groupId>com.github.prifiz</groupId>
<artifactId>scenario</artifactId>
<version>1.0.0</version>
</dependency>
Okay, you've just added the needed dependency to your project. What's next?
It's time to forget that you're developer and start designing the menu.
It is really easy. You can even ask someone without any programming skills to configure it. The only required thing to know here is YAML syntax.
Now let's create a simple menu configuration:
- Create an empty yaml-file with any name you want, e.g. myfirstmenu.yml
- The root element should be "menuSystem:".
- Create a menu by specifying its name and text.
The name should be unique for referencing this menu by the others, the text value will be displayed in your command line app.
The simplest menu configuration will look like this:
menuSystem:
# Home Menu
- name: "home"
text: |
Welcome to my awesome app.
It was quite easy to configure it!
Then we should tell our menu system that the defined menu is the starting point to iterate. To do this, the properties attribute should be added:
menuSystem:
# Home Menu
- name: "home"
properties:
- home
text: |
Welcome to my awesome app.
It was quite easy to configure it!
Currently, only three properties are supported:
- home - menu with this property value is the starting point to iterate over the menu system
- exit - menu with this property value is the end of the menu system. It is used in menu validations.
- noInput - menu with this property value won't require any user input.
This is still not actually a menu system because it doesn't provide any options to choose. Let's add a couple.
menuSystem:
# Home Menu
- name: "home"
properties:
- home
text: |
Welcome to my awesome app.
It was quite easy to configure it!
items:
- name: "option1"
text: "Option One"
- name: "option2"
text: "Option Two"
What happened here:
- our menu will display the welcome text specified in "text" attribute,
- next, the two options will be displayed:
- Option One
- Option Two
But... it is still not a menu system because it can't go to the chosen menu.
To do this, let's add two other menus and link the items with them by "gotoMenu" attribute:
menuSystem:
# Home Menu
- name: "home"
properties:
- home
text: |
Welcome to my awesome app.
It was quite easy to configure it!
items:
- name: "option1"
text: "Option One"
gotoMenu: "menuOne"
- name: "option2"
text: "Option Two"
gotoMenu: "menuTwo"
# Menu One
- name: "menuOne"
text: "Welcome to Menu One!"
# Menu Two
- name: "menuTwo"
text: "Welcome to Menu Two!"
Well done!
Save the myfirstmenu.yml file somewhere to the project resources.
Now we are finally ready for returning to Java code to enable the menu.
Open the class of your app where you want the menu to be called.
import org.prifizapps.walkers.MenuWalkerInitiator;
import java.io.IOException;
import java.io.InputStream;
public class MyFirstMenuClass {
public void runMenuSystem() throws IOException {
InputStream inputStream = this.getClass().getResourceAsStream("myfirstmenu.yml");
MenuWalkerInitiator.initMenu(inputStream).run();
}
}
That's it! You've just added a CLI menu system to your application by typing only 2 lines of java code!
Now you can run your app and type "Option One" for going to Menu One or "Option Two" for going to Menu Two.
Well, now you're able to configure the menu system and run it from your Java app.
But the menu system should interact with application's business logic.
To do this, the adapter classes should be used.
Create a class implementing org.prifizapps.adapters.CommandLineAdapter interface. E.g. if your application is a calculator, the AdditionAdapter class can be created like the following:
// Adapter example
public class AdditionAdapter implements CommandLineAdapter {
private String addFirst;
private String addSecond;
Calculator calculator = new CalculatorImpl();
public String execute() {
int first = Integer.parseInt(addFirst);
int second = Integer.parseInt(addSecond);
return String.valueOf(calculator.add(first, second));
}
}
Here, the Calculator instance is an example of your application class which implements some business logic.
The adapter interacts with the menu system by one of the following ways:
- Menu system can set some adapter's field value
- Menu system can call the adapter's execute() method
- Menu system can first set the field value and then call execute() method
When the adapter is created, the field and methods bindings should be added to menu configuration via "bindings" attribute.
This code binds the specified field:
- name: "addFirst"
text: "Enter first value:"
gotoMenu: "addSecond"
bindings: { field: "addFirst" }
It means that when user inputs the requested first value, the entered value will be set to "addFirst" field of any found adapter class.
If you want to bind a field of the only specific class, you should just add the class name reference before the field name:
bindings: { field: "AdditionAdapter.addFirst" }
⚠️ In the current library version, the multiple adapters can't be specified. For example, you can't configure the following:
bindings: { field: "Adapter1.addFirst, Adapter2.addFirst" }
To bind the execute method, the bindings should be configured like this:
bindings: { runAdapter: "AdditionAdapter" }
This will call the AdditionAdapter.execute() method.
And for both field and method:
bindings: { field: "AdditionAdapter.addFirst", runAdapter: "AdditionAdapter" }
Well, we have an adapter class and the binding configuration. To make this work together, we should register our adapter in the MenuWalker:
MenuWalkerInitiator.initMenu(inputStream)
.registerAdapter(new AdditionAdapter())
.run();
Now the menu system will find our adapter and link it.
That's all! Our menu system is ready to use!
The library provides in-build menu validations.
When the run() method is called, the menu system performs self-diagnostic in order to find some issues with the menu system architecture, such as:
- Dead Ends - menus which lead to nowhere. The only exception is a menu with exit property value.
- Duplicated Frames - menus (or frames) with the equal names. Each menu name should be unique to be correctly referenced.
- Duplicated Items - 2 or more items of the same menu with equal names. Menu options should be unique, otherwise they're not the options.
- Endless Cycles - the situation when while iterating the menu by some route, the exit menu can't be reached.
- Frames (Menus) Without Text - it menu has no text to display, it will disappoint the user.
- Menu Items Without Text - the user is expected to choose the option but doesn't see those options.
- Multiple Home Frames - there should be only one starting point (property: - home) in the menu system. Otherwise, it is impossible to start iterating over it.
By default, all the validations above are enabled. Any validation violation will stop the application with the appropriate error message.
The validations can be disabled:
MenuWalkerInitiator.initMenu(inputStream)
.disableInBuiltValidation()
.run();
One of the most often asked questions about menus is the input text validation, and Scenario library has the answer.
The input checking mechanism is very similar to Menu System Validations but it is applied to the menus instead of all the menu system.
Of course, you can validate the fields values in the specified adapters, but this won't be able to make user enter the value again until it is correct.
How to validate the menu input:
- Create a new class extending org.prifizapps.menuentities.input.AbstractInputRule abstract class (let it be PositiveValueCheck) and implement the following abstract methods:
- String getErrorMessage() - the error message to be displayed if the rule is violated.
E.g. "The value should be positive". - String getRuleDefName() - the rule unique name. The menu system will lookup the rule implementation by this name.
E.g. "PositiveValueCheck" - boolean isPassed(String input) - the rule check implementation. Should return true if input is correct and false otherwise.
E.g. return Integer.parseInt(input) > 0;
- String getErrorMessage() - the error message to be displayed if the rule is violated.
public class PositiveValueCheck extends AbstractInputRule {
@Override
public String getErrorMessage() {
return "The value should be positive";
}
@Override
public String getRuleDefName() {
return "PositiveValueCheck";
}
@Override
public boolean isPassed(String input) {
return Integer.parseInt(input) > 0;
}
}
- Open the menu configuration file (e.g. myfirstmenu.yml from the examples above), find the menu to be validated and configure the input rules for them:
# Add First Number Input
- name: "addFirst"
text: "Enter first value:"
gotoMenu: "addSecond"
inputRules:
- { rule: "PositiveValueCheck", errorMessage: "Non-positive values prohibited! Please try again." }
bindings: { field: "addFirst" }
As you can see, the errorMessage can be defined here, too. If it is specified in yaml config, it will override the return value of getErrorMessage() method, so you can define error messages for the same rule depending on the context.
The "rule" attribute value should correspond the getRuleDefName() method return value.
Oh, of course you can define as many rules as you want:
# Add First Number Input
- name: "addFirst"
text: "Enter first value:"
gotoMenu: "addSecond"
inputRules:
- { rule: "IsNumber", errorMessage: "First value should be a number" }
- { rule: "PositiveValueCheck", errorMessage: "Non-positive values prohibited! Please try again." }
bindings: { field: "addFirst" }
The multiple rules will be processed in the same order as they are defined in config. So for the sample above, the input value first will be checked to be a number (to eliminate the input like "asdsf123omg") and then, if the first check passed, checked to be positive.
- And finally your rules classes should be registered like it was made earlier with Menu Validators:
AbstractInputRule positiveValueCheck = new PositiveValueCheck();
MenuWalkerInitiator.initMenu(inputStream)
.withCustomInputProcessors(positive)
.run();
That's it! The input processing is enabled.
To choose one of the menu items, user will have to type the text (of course, case-insensitive) equal to the corresponding item's "text" attribute value.
For example, to choose the "No" option in the menu below, the user will have to type "No" text:
- name: "exitWithConfirm"
text: "Do you want to exit?"
items:
- name: "yesItem"
text: "Yes"
gotoMenu: "exitMenu"
- name: "noItem"
text: "No"
gotoMenu: "home"
Sometimes text may be long to explain the user the options to choose. In these cases, typing the whole text each time might be slow and annoying.
The Scenario support the shortcuts to deal with such situations.
To add the shortcuts or so-called input alternatives, the "inputAlternatives" should be added on the item level like this:
- name: "exitWithConfirm"
text: "Do you want to exit?"
items:
- name: "yesItem"
text: "Yes"
inputAlternatives:
- "y"
- "yes"
- "+"
gotoMenu: "exitMenu"
- name: "noItem"
text: "No"
inputAlternatives:
- "n"
- "no"
- "-"
gotoMenu: "home"
When the application using this menu configuration is launched, this menu will look like this:
Do you want to exit? Yes (y, yes, +) No (n, no, -)
Here, instead of "Yes" text, any comma-separated case insensitive option can be typed ("y", "Y", "yes", "YES", "Yes", "yEs",..., or "+"). The same logic applies to "No" item.
The example application can be found here: https://github.com/Prifiz/simple-calc-example