Surveys is a plain java library to provide a base for nested questionnaires. It also provides a function to generate and import diagrams using graphviz-java and to measure answer times.
- General
- Flow usage [Flow definition]
- Survey usage [answers, history, transitions]
- DiagramExporter usage [Diagram exporter]
- DiagramImporter usage [Diagram importer]
- Full example
- TODOs
The goal of this project was to build a simple, solid core workflow/state machine library with a minimalistic style. Means everyone can build easily on top of it while providing already basic functions like import/export diagrams. A survey is easy to modify and store in a database as its just a simple ordered list.
- Library
graphviz
(e.g.brew install graphviz
,sudo apt-get install graphviz
) is needed for import and export diagrams. (graphviz-java is used)
On this example:
- Green = Answered
- Orange = Current
- Blue = Transitioned back path
Question flow = Question.of("START")
.targetGet(Question.of("Q1"))
.targetGet(Question.of("Q2"))
.targetGet(Question.of("Q3"))
.targetGet(Question.of("END"));
QuestionBool flow = QuestionBool.of("START");
flow.target(Question.of("OPTION_01"), answer->answer==true);
flow.target(Question.of("OPTION_02"), new MyCondition());
- Back events are functions. They will be triggered on any back transition which needs to step over an associated FlowItem
- Back conditions can block the backward transitions
Question flow = Question.of("Q1");
flow.onBack(answer->answer==true);
flow.onBack(new MyCondition());
public class CustomChoice extends Choice<String> {
//Label for diagram - nullable
public CustomChoice() {
super("If equals 1");
}
//Return true if transition to target is allowed
@Override
public boolean apply(final String answer) {
return answer.equals("1");
}
}
- Default FlowItems/Examples can be found in the JavaDoc
import java.util.Optional;
public class MyFlowItem extends FlowItem<Boolean, MyFlowItem> {
//Parse answer to defined type which will be used to match a condition
@Override
public Optional<String> parse(final ContextExchange exchange) {
return exchange.payload(String.class);
}
}
- Surveys are used for
- Answering the flow
- Tracking the answer history
- Soring the flow config/behavior
Question flow = Question.of(START);
Survey mySurvey = Survey.init(myFlow);
- Surveys answers always the current FlowItem in the flow
" Question flow = Question.of(Q1).target(Question.of(Q2));
Survey survey = Survey.init(myFlow);
survey.answer("Yes") //Answers the first question (Q1)
survey.answer("Yes") //Answers the second question (Q2)
survey.answer("Yes", "My Context object") //Answers the third question with a custom context object
- Export a survey can be useful to save the current state like to a DB
Survey survey = Survey.init(Question.of(MYFLOW));
List<HistoryItem> history = survey.getHistory(); //The order is important - time is UTC
List<HistoryItemJson> history = survey.getHistoryJson(); //converts answers to json for easier database storage
- Importing a survey can be useful to continue a previous survey
List<HistoryItem> history = [...]
Survey survey=Survey.init(history);
- Transitioning back and forth won't lose the answer history
"" Survey survey = [...]
boolean success = survey.transitTo("Q2")
boolean success = survey.transitTo("Q1", "My custom context object")
- On default back transitions without conditions are allowed - this can be disabled by
autoBackTransition
Survey survey = [...]
boolean success = survey.autoBackTransition(false)
- A diagram can be easily rendered of any survey or flowItem (default target = javaTmpDir)
- Java lambda functions are not exportable!
final File path=survey.diagram().save(survey, "/optional/target/file.svg", Format.SVG)
- Directions (TOP_TO_BOTTOM, BOTTOM_TO_TOP, LEFT_TO_RIGHT, RIGHT_TO_LEFT)
final DiagramExporter exporter = survey.diagram();
exporter.config().direction(LEFT_TO_RIGHT);
final DiagramExporter exporter = survey.diagram();
exporter.config().width(800).height(600);
- Graphviz diagram Attributes (e.g. Color, Shape,...) can additionally for each ElementType [ITEM_DRAFT, ITEM_CHOICE, ITEM_CURRENT, ITEM_ANSWERED, ITEM_DEFAULT]
final DiagramExporter exporter = survey.diagram();
exporter.config()
.add(ITEM_CURRENT, Color.RED)
.add(ITEM_ANSWERED, Shape.START);
- Disable autogenerated choice elements
final DiagramExporter exporter = survey.diagram();
exporter.config().add(ITEM_CHOICE, Shape.NONE);
- Surveys can output the time a user spent to answer the questions
Survey survey = [...]
Map<String, Long> durations=survey.getDurationsMS()
- Format must be DOT
- Import can be imported by [File, String, InputStream, MutableGraph]
- Its required to define possible flowItems (Child's of FlowItem) and conditions (Child' of Condition) since the library doesn't use reflections (except of the export to json function)
final FlowItem<?,?> flow = new DiagramImporter().read(file)
- Diagrams can be manually created like with GraphvizOnline
- To detect the FlowItems and Conditions, it's important to add meta attributes
- Link Attributes
DiagramExporter.CONFIG_KEY_SOURCE
= configures the "from" flowItemDiagramExporter.CONFIG_KEY_TARGET
= configures the "to" flowItemDiagramExporter.CONFIG_KEY_CONDITION
= Condition class name
- Element/Node Attributes
DiagramExporter.CONFIG_KEY_SOURCE
= Label for flowItemDiagramExporter.CONFIG_KEY_CLASS
= FlowItem class nameDiagramExporter.CONFIG_KEY_CONDITION
= Comma separated list of condition class names for back transitions
class SurveyExampleTest {
@Test
void testSurvey() {
final QuestionBool flow = QuestionBool.of("START");
final AtomicBoolean question2BackTriggered = new AtomicBoolean(false);
//DEFINE FLOW
flow.target(Question.of("Q1_TRUE"), answer -> answer == true);
flow.targetGet(Question.of("Q1_FALSE"), answer -> answer == false)
.targetGet(Question.of("Q2")).onBack(oldAnswer -> {
question2BackTriggered.set(true);
return true;
})
.targetGet(Question.of("Q3"))
.targetGet(Question.of("END"));
//CREATE survey that manages the history / context
final Survey survey01 = Survey.init(flow);
//EXECUTE survey flow
assertThat(survey01.get(), is(equalTo(QuestionBool.of("START"))));
assertThat(survey01.answer("Yes").get(), is(equalTo(Question.of("Q1_TRUE"))));
assertThat(survey01.transitTo("START"), is(true));
//TRANSITION NO BACK TRIGGERED
assertThat(question2BackTriggered.get(), is(false));
assertThat(survey01.get(), is(equalTo(QuestionBool.of("START"))));
assertThat(survey01.answer("No").get(), is(equalTo(Question.of("Q1_FALSE"))));
//EXPORT / IMPORT
final List<HistoryItemJson> export = survey01.getHistoryJson();
final Survey survey02 = Survey.init(flow, export);
assertThat(export, is(equalTo(survey02.getHistoryJson())));
assertThat(survey02.get(), is(equalTo(survey01.get())));
assertThat(survey02.answer("next").get(), is(equalTo(Question.of("Q2"))));
assertThat(survey02.answer("next").get(), is(equalTo(Question.of("Q3"))));
assertThat(survey02.answer("next").get(), is(equalTo(Question.of("END"))));
assertThat(survey02.answer("next").get(), is(equalTo(Question.of("END"))));
assertThat(survey02.isEnded(), is(true));
//TRANSITION BACK TRIGGERED
assertThat(survey02.transitTo("START"), is(true));
assertThat(question2BackTriggered.get(), is(true));
assertThat(survey02.isEnded(), is(false));
//TRANSITION FORWARD
assertThat(survey02.transitTo("END"), is(true));
assertThat(survey02.get(), is(Question.of("END")));
assertThat(survey02.isEnded(), is(true));
//IMPORT FINISHED FLOW
assertThat(Survey.init(flow, survey02.getHistory()).isEnded(), is(true));
}
}
- Core: Implement custom exceptions
- Feature: Custom error handler concept
- Feature: Add more question examples like radio, checkbox, list, map,...
- Diagram: Generate heat map from List of Surveys for e.g. most, longest, never taken answers
- Diagram: Custom styling also for links
- Diagram: Import / Export from UML