InteractivePrompter.java
package com.cloudforge.core.config;
import com.cloudforge.core.interfaces.ApplicationSpec;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.List;
/**
* Interactive prompting utility that generates questions from @ConfigField annotations.
*
* <p>Uses {@link ConfigurationIntrospector} to discover fields and automatically
* generates appropriate prompts based on field metadata.</p>
*
* <h2>Usage</h2>
* <pre>{@code
* DeploymentConfig config = new DeploymentConfig();
* InteractivePrompter prompter = new InteractivePrompter(System.in, System.out);
* prompter.promptForConfiguration(config, applicationSpec);
* }</pre>
*
* @since 3.0.0
*/
public class InteractivePrompter {
private final BufferedReader reader;
private final PrintStream output;
/**
* Creates a new InteractivePrompter with the specified input and output streams.
*
* @param input input stream for reading user responses
* @param output output stream for displaying prompts
*/
public InteractivePrompter(InputStream input, PrintStream output) {
this.reader = new BufferedReader(new InputStreamReader(input));
this.output = output;
}
/**
* Prompts for all visible configuration fields.
*
* <p>Iterates through visible fields (based on application capabilities and
* current configuration state) and prompts for values.</p>
*
* @param config the configuration object to populate
* @param appSpec the application spec for determining field visibility
*/
public void promptForConfiguration(DeploymentConfig config, ApplicationSpec appSpec) {
List<ConfigFieldInfo> fields = ConfigurationIntrospector.discoverVisibleFields(appSpec, config);
for (ConfigFieldInfo field : fields) {
promptForField(field, config, appSpec);
// Re-evaluate visible fields after each field change
// (some fields may become visible/hidden based on values)
fields = ConfigurationIntrospector.discoverVisibleFields(appSpec, config);
}
}
/**
* Prompts for fields in a specific category.
*
* @param config the configuration object to populate
* @param appSpec the application spec
* @param category the category to prompt for
*/
public void promptForCategory(DeploymentConfig config, ApplicationSpec appSpec, String category) {
List<ConfigFieldInfo> fields = ConfigurationIntrospector.discoverVisibleFields(appSpec, config, category);
for (ConfigFieldInfo field : fields) {
promptForField(field, config, appSpec);
}
}
/**
* Prompts for a single field.
*/
private void promptForField(ConfigFieldInfo field, Object config, ApplicationSpec appSpec) {
// Skip if field is not visible
if (!field.isVisible(appSpec, config)) {
return;
}
Class<?> type = field.type();
if (field.allowedValues().length > 0) {
promptChoice(field, config);
} else if (type == boolean.class || type == Boolean.class) {
promptYesNo(field, config);
} else if (isNumericType(type)) {
promptNumeric(field, config);
} else {
promptString(field, config);
}
}
/**
* Prompts for a choice from allowed values.
*/
private void promptChoice(ConfigFieldInfo field, Object config) {
String[] choices = field.allowedValues();
Object currentValue = field.getValue(config);
String defaultValue = currentValue != null ? currentValue.toString() : "";
output.println();
output.println("📋 " + field.displayName());
if (!field.description().isEmpty()) {
output.println(" " + field.description());
}
// Find default index
int defaultIndex = 0;
for (int i = 0; i < choices.length; i++) {
String marker = choices[i].equals(defaultValue) ? " (default)" : "";
output.printf(" [%d] %s%s%n", i + 1, choices[i], marker);
if (choices[i].equals(defaultValue)) {
defaultIndex = i + 1;
}
}
String prompt = String.format("Select [1-%d] (default: %d): ", choices.length, defaultIndex);
String response = readLine(prompt);
String value;
if (response.isEmpty()) {
value = defaultValue.isEmpty() ? choices[0] : defaultValue;
} else {
try {
int index = Integer.parseInt(response) - 1;
if (index >= 0 && index < choices.length) {
value = choices[index];
} else {
output.println("❌ Invalid selection, using default");
value = defaultValue.isEmpty() ? choices[0] : defaultValue;
}
} catch (NumberFormatException e) {
// Check if they typed the value directly
boolean found = false;
for (String choice : choices) {
if (choice.equalsIgnoreCase(response)) {
value = choice;
found = true;
break;
}
}
if (!found) {
output.println("❌ Invalid selection, using default");
value = defaultValue.isEmpty() ? choices[0] : defaultValue;
} else {
// value already set in loop
value = response;
}
}
}
setFieldValue(field, config, value);
output.println("✅ " + field.displayName() + ": " + value);
}
/**
* Prompts for a yes/no boolean value.
*/
private void promptYesNo(ConfigFieldInfo field, Object config) {
Object currentValue = field.getValue(config);
boolean defaultValue = currentValue != null ? (Boolean) currentValue : false;
output.println();
output.println("📋 " + field.displayName());
if (!field.description().isEmpty()) {
output.println(" " + field.description());
}
String defaultStr = defaultValue ? "Y" : "N";
String prompt = String.format("%s [y/n] (default: %s): ", field.displayName(), defaultStr);
String response = readLine(prompt).toLowerCase();
boolean value;
if (response.isEmpty()) {
value = defaultValue;
} else if (response.startsWith("y")) {
value = true;
} else if (response.startsWith("n")) {
value = false;
} else {
output.println("❌ Invalid response, using default: " + defaultValue);
value = defaultValue;
}
field.setValue(config, value);
output.println("✅ " + field.displayName() + ": " + (value ? "Yes" : "No"));
}
/**
* Prompts for a numeric value with validation.
*/
private void promptNumeric(ConfigFieldInfo field, Object config) {
Object currentValue = field.getValue(config);
String defaultStr = currentValue != null ? currentValue.toString() : "";
output.println();
output.println("📋 " + field.displayName());
if (!field.description().isEmpty()) {
output.println(" " + field.description());
}
// Show range if specified
if (field.min() > Double.NEGATIVE_INFINITY || field.max() < Double.POSITIVE_INFINITY) {
String range = String.format(" Range: %.0f - %.0f", field.min(), field.max());
output.println(range);
}
String prompt = String.format("%s (default: %s): ", field.displayName(), defaultStr);
String response = readLine(prompt);
if (response.isEmpty()) {
// Keep current value
output.println("✅ " + field.displayName() + ": " + defaultStr);
return;
}
try {
Number value = parseNumber(response, field.type());
// Validate range
ValidationResult result = field.validate(value, config);
if (result.isError()) {
output.println("❌ " + result.getMessage() + ", using default");
return;
}
setFieldValue(field, config, value);
output.println("✅ " + field.displayName() + ": " + value);
} catch (NumberFormatException e) {
output.println("❌ Invalid number, using default");
}
}
/**
* Prompts for a string value.
*/
private void promptString(ConfigFieldInfo field, Object config) {
Object currentValue = field.getValue(config);
String defaultValue = currentValue != null ? currentValue.toString() : "";
output.println();
output.println("📋 " + field.displayName());
if (!field.description().isEmpty()) {
output.println(" " + field.description());
}
if (!field.example().isEmpty()) {
output.println(" Example: " + field.example());
}
String defaultDisplay = defaultValue.isEmpty() ? "(none)" : defaultValue;
String prompt = String.format("%s (default: %s): ", field.displayName(), defaultDisplay);
String response;
if (field.sensitive()) {
response = readLineSecure(prompt);
} else {
response = readLine(prompt);
}
String value = response.isEmpty() ? defaultValue : response;
// Validate pattern if specified
if (!field.pattern().isEmpty() && !value.isEmpty()) {
if (!value.matches(field.pattern())) {
output.println("❌ Value does not match required pattern: " + field.pattern());
output.println(" Using default: " + defaultValue);
value = defaultValue;
}
}
// Validate required
if (field.required() && value.isEmpty()) {
output.println("❌ This field is required");
promptString(field, config); // Retry
return;
}
field.setValue(config, value.isEmpty() ? null : value);
String displayValue = field.sensitive() ? "********" : value;
output.println("✅ " + field.displayName() + ": " + (displayValue.isEmpty() ? "(not set)" : displayValue));
}
/**
* Reads a line of input from the user.
*/
private String readLine(String prompt) {
output.print(prompt);
output.flush();
try {
String line = reader.readLine();
return line != null ? line.trim() : "";
} catch (IOException e) {
return "";
}
}
/**
* Reads a line of input securely (for sensitive fields).
* Falls back to regular input if console not available.
*/
private String readLineSecure(String prompt) {
if (System.console() != null) {
char[] chars = System.console().readPassword(prompt);
return chars != null ? new String(chars) : "";
}
return readLine(prompt);
}
/**
* Sets a field value with appropriate type conversion.
*/
private void setFieldValue(ConfigFieldInfo field, Object config, Object value) {
Class<?> type = field.type();
if (type.isEnum() && value instanceof String) {
@SuppressWarnings({"unchecked", "rawtypes"})
Object enumValue = Enum.valueOf((Class<Enum>) type, ((String) value).toUpperCase());
field.setValue(config, enumValue);
} else if (isNumericType(type) && value instanceof String) {
field.setValue(config, parseNumber((String) value, type));
} else {
field.setValue(config, value);
}
}
/**
* Checks if a type is numeric.
*/
private boolean isNumericType(Class<?> type) {
return type == int.class || type == Integer.class ||
type == long.class || type == Long.class ||
type == double.class || type == Double.class ||
type == float.class || type == Float.class;
}
/**
* Parses a string to a number of the specified type.
*/
private Number parseNumber(String value, Class<?> type) {
if (type == int.class || type == Integer.class) {
return Integer.parseInt(value);
} else if (type == long.class || type == Long.class) {
return Long.parseLong(value);
} else if (type == double.class || type == Double.class) {
return Double.parseDouble(value);
} else if (type == float.class || type == Float.class) {
return Float.parseFloat(value);
}
return Integer.parseInt(value);
}
}