diff --git a/aesh-processor/src/main/java/org/aesh/processor/AeshAnnotationProcessor.java b/aesh-processor/src/main/java/org/aesh/processor/AeshAnnotationProcessor.java index 62369179..e39f1c0b 100644 --- a/aesh-processor/src/main/java/org/aesh/processor/AeshAnnotationProcessor.java +++ b/aesh-processor/src/main/java/org/aesh/processor/AeshAnnotationProcessor.java @@ -211,13 +211,21 @@ private void collectFieldsRecursive(TypeElement element, List f private void generateProvider(TypeElement commandElement) throws IOException { String qualifiedName = commandElement.getQualifiedName().toString(); - String packageName = ""; - int lastDot = qualifiedName.lastIndexOf('.'); - if (lastDot > 0) { - packageName = qualifiedName.substring(0, lastDot); + String packageName = elementUtils.getPackageOf(commandElement).getQualifiedName().toString(); + + // For inner classes (e.g., Config.Set), we need: + // - typeRefName: "Config.Set" for use in generated code type references + // - metadataClassName: "Config_Set_AeshMetadata" to avoid clashing with enclosing class + String typeRefName; + String metadataClassName; + if (packageName.isEmpty()) { + typeRefName = qualifiedName; + metadataClassName = qualifiedName.replace('.', '_') + "_AeshMetadata"; + } else { + // Strip package prefix to get e.g. "Config.Set" for inner classes or "Batch" for top-level + typeRefName = qualifiedName.substring(packageName.length() + 1); + metadataClassName = typeRefName.replace('.', '_') + "_AeshMetadata"; } - String simpleName = commandElement.getSimpleName().toString(); - String metadataClassName = simpleName + "_AeshMetadata"; String fullMetadataName = packageName.isEmpty() ? metadataClassName : packageName + "." + metadataClassName; boolean isGroup = commandElement.getAnnotation(GroupCommandDefinition.class) != null; @@ -225,7 +233,7 @@ private void generateProvider(TypeElement commandElement) throws IOException { List fields = collectFields(commandElement); String code = CodeGenerator.generate( - packageName, simpleName, metadataClassName, qualifiedName, + packageName, typeRefName, metadataClassName, qualifiedName, commandElement, fields, isGroup, elementUtils, typeUtils); JavaFileObject sourceFile = filer.createSourceFile(fullMetadataName, commandElement); diff --git a/aesh-processor/src/main/java/org/aesh/processor/CodeGenerator.java b/aesh-processor/src/main/java/org/aesh/processor/CodeGenerator.java index 4bd07076..39683295 100644 --- a/aesh-processor/src/main/java/org/aesh/processor/CodeGenerator.java +++ b/aesh-processor/src/main/java/org/aesh/processor/CodeGenerator.java @@ -73,7 +73,9 @@ static String generate( } // Imports - sb.append("import java.util.Arrays;\n\n"); + sb.append("import java.util.Arrays;\n"); + sb.append("import java.util.function.BiConsumer;\n"); + sb.append("import java.util.function.Consumer;\n\n"); sb.append("import org.aesh.command.Command;\n"); sb.append("import org.aesh.command.impl.internal.ProcessedCommand;\n"); sb.append("import org.aesh.command.impl.internal.ProcessedCommandBuilder;\n"); @@ -123,7 +125,7 @@ static String generate( sb.append(" @Override\n"); sb.append(" public ProcessedCommand buildProcessedCommand(").append(simpleName).append(" instance)"); sb.append(" throws CommandLineParserException {\n"); - generateBuildProcessedCommand(sb, commandElement, fields, isGroup, elementUtils, typeUtils); + generateBuildProcessedCommand(sb, simpleName, commandElement, fields, isGroup, elementUtils, typeUtils); sb.append(" }\n"); // End class @@ -172,11 +174,12 @@ private static List getGroupCommandClassNames(TypeElement element, Eleme return classNames; } - private static void generateBuildProcessedCommand(StringBuilder sb, TypeElement commandElement, + private static void generateBuildProcessedCommand(StringBuilder sb, String simpleName, TypeElement commandElement, List fields, boolean isGroup, Elements elementUtils, Types typeUtils) { - sb.append(" ProcessedCommand processedCommand = ProcessedCommandBuilder.builder()\n"); + sb.append( + " ProcessedCommand processedCommand = ((ProcessedCommandBuilder) ProcessedCommandBuilder.builder())\n"); if (isGroup) { GroupCommandDefinition gcd = commandElement.getAnnotation(GroupCommandDefinition.class); @@ -209,7 +212,7 @@ private static void generateBuildProcessedCommand(StringBuilder sb, TypeElement // Process fields for (VariableElement field : fields) { - generateFieldProcessing(sb, field, elementUtils, typeUtils); + generateFieldProcessing(sb, simpleName, field, elementUtils, typeUtils); } sb.append(" return processedCommand;\n"); @@ -245,7 +248,7 @@ private static void generateCommandActivator(StringBuilder sb, TypeElement eleme } } - private static void generateFieldProcessing(StringBuilder sb, VariableElement field, + private static void generateFieldProcessing(StringBuilder sb, String simpleName, VariableElement field, Elements elementUtils, Types typeUtils) { Option o = field.getAnnotation(Option.class); OptionList ol = field.getAnnotation(OptionList.class); @@ -254,19 +257,19 @@ private static void generateFieldProcessing(StringBuilder sb, VariableElement fi Argument arg = field.getAnnotation(Argument.class); if (o != null) { - generateOption(sb, field, o, elementUtils, typeUtils); + generateOption(sb, simpleName, field, o, elementUtils, typeUtils); } else if (ol != null) { - generateOptionList(sb, field, ol, elementUtils, typeUtils); + generateOptionList(sb, simpleName, field, ol, elementUtils, typeUtils); } else if (og != null) { - generateOptionGroup(sb, field, og, elementUtils, typeUtils); + generateOptionGroup(sb, simpleName, field, og, elementUtils, typeUtils); } else if (args != null) { - generateArguments(sb, field, args, elementUtils, typeUtils); + generateArguments(sb, simpleName, field, args, elementUtils, typeUtils); } else if (arg != null) { - generateArgument(sb, field, arg, elementUtils, typeUtils); + generateArgument(sb, simpleName, field, arg, elementUtils, typeUtils); } } - private static void generateOption(StringBuilder sb, VariableElement field, Option o, + private static void generateOption(StringBuilder sb, String simpleName, VariableElement field, Option o, Elements elementUtils, Types typeUtils) { String fieldName = field.getSimpleName().toString(); String fieldType = getBoxedTypeName(field.asType(), typeUtils); @@ -308,10 +311,12 @@ private static void generateOption(StringBuilder sb, VariableElement field, Opti sb.append(" .inherited(").append(o.inherited()).append(")\n"); sb.append(" .descriptionUrl(").append(stringLiteral(o.descriptionUrl())).append(")\n"); sb.append(" .url(").append(o.url()).append(")\n"); + generateFieldSetter(sb, simpleName, field, typeUtils); + generateFieldResetter(sb, simpleName, field, typeUtils); sb.append(" .build());\n\n"); } - private static void generateOptionList(StringBuilder sb, VariableElement field, OptionList ol, + private static void generateOptionList(StringBuilder sb, String simpleName, VariableElement field, OptionList ol, Elements elementUtils, Types typeUtils) { String fieldName = field.getSimpleName().toString(); String elementType = getGenericTypeArgument(field.asType(), 0, typeUtils); @@ -337,10 +342,12 @@ private static void generateOptionList(StringBuilder sb, VariableElement field, generateOptionActivator(sb, field, "activator", elementUtils); generateOptionRenderer(sb, field, "renderer", elementUtils); generateOptionParser(sb, field, "parser", elementUtils); + generateFieldSetter(sb, simpleName, field, typeUtils); + generateFieldResetter(sb, simpleName, field, typeUtils); sb.append(" .build());\n\n"); } - private static void generateOptionGroup(StringBuilder sb, VariableElement field, OptionGroup og, + private static void generateOptionGroup(StringBuilder sb, String simpleName, VariableElement field, OptionGroup og, Elements elementUtils, Types typeUtils) { String fieldName = field.getSimpleName().toString(); // For Map, extract V (index 1) @@ -366,10 +373,12 @@ private static void generateOptionGroup(StringBuilder sb, VariableElement field, generateOptionActivator(sb, field, "activator", elementUtils); generateOptionRenderer(sb, field, "renderer", elementUtils); generateOptionParser(sb, field, "parser", elementUtils); + generateFieldSetter(sb, simpleName, field, typeUtils); + generateFieldResetter(sb, simpleName, field, typeUtils); sb.append(" .build());\n\n"); } - private static void generateArguments(StringBuilder sb, VariableElement field, Arguments a, + private static void generateArguments(StringBuilder sb, String simpleName, VariableElement field, Arguments a, Elements elementUtils, Types typeUtils) { String fieldName = field.getSimpleName().toString(); String elementType = getGenericTypeArgument(field.asType(), 0, typeUtils); @@ -394,10 +403,12 @@ private static void generateArguments(StringBuilder sb, VariableElement field, A generateOptionActivator(sb, field, "activator", elementUtils); generateOptionParser(sb, field, "parser", elementUtils); sb.append(" .url(").append(a.url()).append(")\n"); + generateFieldSetter(sb, simpleName, field, typeUtils); + generateFieldResetter(sb, simpleName, field, typeUtils); sb.append(" .build());\n\n"); } - private static void generateArgument(StringBuilder sb, VariableElement field, Argument arg, + private static void generateArgument(StringBuilder sb, String simpleName, VariableElement field, Argument arg, Elements elementUtils, Types typeUtils) { String fieldName = field.getSimpleName().toString(); String fieldType = getBoxedTypeName(field.asType(), typeUtils); @@ -425,6 +436,8 @@ private static void generateArgument(StringBuilder sb, VariableElement field, Ar sb.append(" .overrideRequired(").append(arg.overrideRequired()).append(")\n"); sb.append(" .inherited(").append(arg.inherited()).append(")\n"); sb.append(" .url(").append(arg.url()).append(")\n"); + generateFieldSetter(sb, simpleName, field, typeUtils); + generateFieldResetter(sb, simpleName, field, typeUtils); sb.append(" .build());\n\n"); } @@ -652,6 +665,64 @@ private static String selectorLiteral(org.aesh.selector.SelectorType selectorTyp return "org.aesh.selector.SelectorType." + selectorType.name(); } + private static boolean isAccessibleField(VariableElement field) { + java.util.Set modifiers = field.getModifiers(); + return !modifiers.contains(javax.lang.model.element.Modifier.PRIVATE); + } + + private static void generateFieldSetter(StringBuilder sb, String commandSimpleName, VariableElement field, + Types typeUtils) { + if (!isAccessibleField(field)) + return; + String fieldName = field.getSimpleName().toString(); + String fieldType = field.asType().toString(); + + sb.append(" .fieldSetter((inst, val) -> ((").append(commandSimpleName).append(") inst).") + .append(fieldName).append(" = (").append(fieldType).append(") val)\n"); + } + + private static void generateFieldResetter(StringBuilder sb, String commandSimpleName, VariableElement field, + Types typeUtils) { + if (!isAccessibleField(field)) + return; + String fieldName = field.getSimpleName().toString(); + TypeMirror fieldType = field.asType(); + + sb.append(" .fieldResetter(inst -> ((").append(commandSimpleName).append(") inst).") + .append(fieldName).append(" = "); + + switch (fieldType.getKind()) { + case BOOLEAN: + sb.append("false"); + break; + case BYTE: + sb.append("(byte) 0"); + break; + case SHORT: + sb.append("(short) 0"); + break; + case INT: + sb.append("0"); + break; + case LONG: + sb.append("0L"); + break; + case FLOAT: + sb.append("0.0f"); + break; + case DOUBLE: + sb.append("0.0d"); + break; + case CHAR: + sb.append("'\\u0000'"); + break; + default: + sb.append("null"); + break; + } + sb.append(")\n"); + } + private static String escapeJavaString(String s) { StringBuilder sb = new StringBuilder(); for (char c : s.toCharArray()) { diff --git a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedCommand.java b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedCommand.java index 62fff729..156016b3 100644 --- a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedCommand.java +++ b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedCommand.java @@ -135,26 +135,36 @@ public List getAliases() { } public void addOption(ProcessedOption opt) throws OptionParserException { - this.options.add(new ProcessedOption(verifyThatNamesAreUnique(opt.shortName(), opt.name()), opt.name(), + ProcessedOption copy = new ProcessedOption(verifyThatNamesAreUnique(opt.shortName(), opt.name()), opt.name(), opt.description(), opt.getArgument(), opt.isRequired(), opt.getValueSeparator(), opt.askIfNotSet(), opt.acceptNameWithoutDashes(), opt.selectorType(), opt.getDefaultValues(), opt.type(), opt.getFieldName(), opt.getOptionType(), opt.converter(), opt.completer(), opt.validator(), opt.activator(), opt.getRenderer(), opt.parser(), opt.doOverrideRequired(), opt.isNegatable(), opt.getNegationPrefix(), opt.isInherited(), - opt.getDescriptionUrl(), opt.isUrl())); + opt.getDescriptionUrl(), opt.isUrl()); + if (opt.getFieldSetter() != null) + copy.setFieldSetter(opt.getFieldSetter()); + if (opt.getFieldResetter() != null) + copy.setFieldResetter(opt.getFieldResetter()); + this.options.add(copy); options.get(options.size() - 1).setParent(this); } private void setOptions(List options) throws OptionParserException { for (ProcessedOption opt : options) { - this.options.add(new ProcessedOption(verifyThatNamesAreUnique(opt.shortName(), opt.name()), opt.name(), + ProcessedOption copy = new ProcessedOption(verifyThatNamesAreUnique(opt.shortName(), opt.name()), opt.name(), opt.description(), opt.getArgument(), opt.isRequired(), opt.getValueSeparator(), opt.askIfNotSet(), opt.acceptNameWithoutDashes(), opt.selectorType(), opt.getDefaultValues(), opt.type(), opt.getFieldName(), opt.getOptionType(), opt.converter(), opt.completer(), opt.validator(), opt.activator(), opt.getRenderer(), opt.parser(), opt.doOverrideRequired(), opt.isNegatable(), opt.getNegationPrefix(), opt.isInherited(), - opt.getDescriptionUrl(), opt.isUrl())); + opt.getDescriptionUrl(), opt.isUrl()); + if (opt.getFieldSetter() != null) + copy.setFieldSetter(opt.getFieldSetter()); + if (opt.getFieldResetter() != null) + copy.setFieldResetter(opt.getFieldResetter()); + this.options.add(copy); this.options.get(this.options.size() - 1).setParent(this); } diff --git a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOption.java b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOption.java index f042cb8f..34426f70 100644 --- a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOption.java +++ b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOption.java @@ -31,6 +31,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import org.aesh.command.activator.OptionActivator; import org.aesh.command.completer.OptionCompleter; @@ -92,6 +94,8 @@ public final class ProcessedOption { private boolean inherited = false; private String descriptionUrl; private boolean isUrl = false; + private BiConsumer fieldSetter; + private Consumer fieldResetter; public ProcessedOption(char shortName, String name, String description, String argument, boolean required, char valueSeparator, boolean askIfNotSet, boolean acceptNameWithoutDashes, @@ -159,6 +163,60 @@ public ProcessedOption(char shortName, String name, String description, values = new ArrayList<>(); } + public void setFieldSetter(BiConsumer fieldSetter) { + this.fieldSetter = fieldSetter; + } + + public void setFieldResetter(Consumer fieldResetter) { + this.fieldResetter = fieldResetter; + } + + public BiConsumer getFieldSetter() { + return fieldSetter; + } + + public Consumer getFieldResetter() { + return fieldResetter; + } + + public void resetField(Object instance) { + if (fieldResetter != null) { + fieldResetter.accept(instance); + return; + } + // Fallback to reflection + try { + Field field = getField(instance.getClass(), fieldName); + if (field == null) + return; + if (!Modifier.isPublic(field.getModifiers())) + field.setAccessible(true); + if (field.getType().isPrimitive()) { + if (boolean.class.isAssignableFrom(field.getType())) + field.set(instance, false); + else if (int.class.isAssignableFrom(field.getType())) + field.set(instance, 0); + else if (short.class.isAssignableFrom(field.getType())) + field.set(instance, (short) 0); + else if (char.class.isAssignableFrom(field.getType())) + field.set(instance, '\u0000'); + else if (byte.class.isAssignableFrom(field.getType())) + field.set(instance, (byte) 0); + else if (long.class.isAssignableFrom(field.getType())) + field.set(instance, 0L); + else if (float.class.isAssignableFrom(field.getType())) + field.set(instance, 0.0f); + else if (double.class.isAssignableFrom(field.getType())) + field.set(instance, 0.0d); + } else if (!hasValue() && field.getType().equals(Boolean.class)) { + field.set(instance, Boolean.FALSE); + } else + field.set(instance, null); + } catch (NoSuchFieldException | IllegalAccessException e) { + // Field reset failed, continue + } + } + public String shortName() { return shortName; } @@ -516,6 +574,49 @@ public void injectValueIntoField(Object instance, InvocationProviders invocation boolean doValidation) throws OptionValidatorException { if (converter == null || instance == null) return; + if (fieldSetter != null) { + injectValueWithSetter(instance, invocationProviders, aeshContext, doValidation); + } else { + injectValueWithReflection(instance, invocationProviders, aeshContext, doValidation); + } + } + + private void injectValueWithSetter(Object instance, InvocationProviders invocationProviders, AeshContext aeshContext, + boolean doValidation) throws OptionValidatorException { + if (optionType == OptionType.NORMAL || optionType == OptionType.BOOLEAN || optionType == OptionType.ARGUMENT) { + if (negatedByUser && optionType == OptionType.BOOLEAN) { + fieldSetter.accept(instance, doConvert("false", invocationProviders, instance, aeshContext, doValidation)); + } else if (getValue() != null) + fieldSetter.accept(instance, doConvert(getValue(), invocationProviders, instance, aeshContext, doValidation)); + else if (defaultValues.size() > 0) { + fieldSetter.accept(instance, + doConvert(defaultValues.get(0), invocationProviders, instance, aeshContext, doValidation)); + } + } else if (optionType == OptionType.LIST || optionType == OptionType.ARGUMENTS) { + Collection tmpSet; + if (Set.class.isAssignableFrom(type)) + tmpSet = new HashSet<>(); + else + tmpSet = new ArrayList<>(); + if (values.size() > 0) { + for (String in : values) + tmpSet.add(doConvert(in, invocationProviders, instance, aeshContext, doValidation)); + } else if (defaultValues.size() > 0) { + for (String in : defaultValues) + tmpSet.add(doConvert(in, invocationProviders, instance, aeshContext, doValidation)); + } + fieldSetter.accept(instance, tmpSet); + } else if (optionType == OptionType.GROUP) { + Map tmpMap = newHashMap(); + for (String propertyKey : properties.keySet()) + tmpMap.put(propertyKey, doConvert(properties.get(propertyKey), invocationProviders, instance, + aeshContext, doValidation)); + fieldSetter.accept(instance, tmpMap); + } + } + + private void injectValueWithReflection(Object instance, InvocationProviders invocationProviders, AeshContext aeshContext, + boolean doValidation) throws OptionValidatorException { try { Field field = getField(instance.getClass(), fieldName); //for some options, the field might be null. eg generatedHelp diff --git a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java index 5770b32f..25a8b864 100644 --- a/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java +++ b/aesh/src/main/java/org/aesh/command/impl/internal/ProcessedOptionBuilder.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.BiConsumer; import java.util.function.Consumer; import org.aesh.command.activator.OptionActivator; @@ -80,6 +81,8 @@ public class ProcessedOptionBuilder { private boolean inherited = false; private String descriptionUrl; private boolean isUrl = false; + private BiConsumer fieldSetter; + private Consumer fieldResetter; private ProcessedOptionBuilder() { defaultValues = new ArrayList<>(); @@ -339,6 +342,14 @@ public ProcessedOptionBuilder url(boolean isUrl) { return apply(c -> c.isUrl = isUrl); } + public ProcessedOptionBuilder fieldSetter(BiConsumer fieldSetter) { + return apply(c -> c.fieldSetter = fieldSetter); + } + + public ProcessedOptionBuilder fieldResetter(Consumer fieldResetter) { + return apply(c -> c.fieldResetter = fieldResetter); + } + public ProcessedOption build() throws OptionParserException { if (optionType == null) { if (!hasValue) @@ -390,10 +401,15 @@ else if (hasMultipleValues) throw new OptionParserException("Option '" + name + "' is marked as negatable but is not a boolean type"); } - return new ProcessedOption(shortName, name, description, argument, required, + ProcessedOption option = new ProcessedOption(shortName, name, description, argument, required, valueSeparator, askIfNotSet, acceptNameWithoutDashes, selectorType, defaultValues, type, fieldName, optionType, converter, completer, validator, activator, renderer, parser, overrideRequired, negatable, negationPrefix, inherited, descriptionUrl, isUrl); + if (fieldSetter != null) + option.setFieldSetter(fieldSetter); + if (fieldResetter != null) + option.setFieldResetter(fieldResetter); + return option; } } diff --git a/aesh/src/main/java/org/aesh/command/impl/populator/AeshCommandPopulator.java b/aesh/src/main/java/org/aesh/command/impl/populator/AeshCommandPopulator.java index 45d64f07..ab9b04dd 100644 --- a/aesh/src/main/java/org/aesh/command/impl/populator/AeshCommandPopulator.java +++ b/aesh/src/main/java/org/aesh/command/impl/populator/AeshCommandPopulator.java @@ -75,7 +75,7 @@ else if (option.getDefaultValues().size() > 0 && option.selectorType() == Select option.injectValueIntoField(getObject(), invocationProviders, aeshContext, mode == CommandLineParser.Mode.VALIDATE); else - resetField(getObject(), option.getFieldName(), option.hasValue()); + option.resetField(getObject()); } //arguments if (processedCommand.getArguments() != null && @@ -84,7 +84,7 @@ else if (option.getDefaultValues().size() > 0 && option.selectorType() == Select processedCommand.getArguments().injectValueIntoField(getObject(), invocationProviders, aeshContext, mode == CommandLineParser.Mode.VALIDATE); else if (processedCommand.getArguments() != null) - resetField(getObject(), processedCommand.getArguments().getFieldName(), true); + processedCommand.getArguments().resetField(getObject()); //argument if (processedCommand.getArgument() != null && (processedCommand.getArgument().getValues().size() > 0 || @@ -92,7 +92,7 @@ else if (processedCommand.getArguments() != null) processedCommand.getArgument().injectValueIntoField(getObject(), invocationProviders, aeshContext, mode == CommandLineParser.Mode.VALIDATE); else if (processedCommand.getArgument() != null) - resetField(getObject(), processedCommand.getArgument().getFieldName(), true); + processedCommand.getArgument().resetField(getObject()); } @Override diff --git a/aesh/src/test/java/org/aesh/command/populator/FieldSetterPopulatorTest.java b/aesh/src/test/java/org/aesh/command/populator/FieldSetterPopulatorTest.java new file mode 100644 index 00000000..3b37a094 --- /dev/null +++ b/aesh/src/test/java/org/aesh/command/populator/FieldSetterPopulatorTest.java @@ -0,0 +1,241 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.aesh.command.populator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.aesh.command.Command; +import org.aesh.command.CommandDefinition; +import org.aesh.command.CommandResult; +import org.aesh.command.impl.activator.AeshCommandActivatorProvider; +import org.aesh.command.impl.activator.AeshOptionActivatorProvider; +import org.aesh.command.impl.completer.AeshCompleterInvocationProvider; +import org.aesh.command.impl.converter.AeshConverterInvocationProvider; +import org.aesh.command.impl.internal.OptionType; +import org.aesh.command.impl.internal.ProcessedCommand; +import org.aesh.command.impl.internal.ProcessedCommandBuilder; +import org.aesh.command.impl.internal.ProcessedOptionBuilder; +import org.aesh.command.impl.invocation.AeshInvocationProviders; +import org.aesh.command.impl.parser.CommandLineParser; +import org.aesh.command.impl.parser.CommandLineParserBuilder; +import org.aesh.command.impl.validator.AeshValidatorInvocationProvider; +import org.aesh.command.invocation.CommandInvocation; +import org.aesh.command.invocation.InvocationProviders; +import org.aesh.command.option.Option; +import org.aesh.command.settings.SettingsBuilder; +import org.aesh.console.AeshContext; +import org.junit.Test; + +/** + * Tests that field setter/resetter lambdas on ProcessedOption work correctly + * with the AeshCommandPopulator, simulating what the annotation processor generates. + */ +public class FieldSetterPopulatorTest { + + private final InvocationProviders invocationProviders = new AeshInvocationProviders( + SettingsBuilder.builder() + .converterInvocationProvider(new AeshConverterInvocationProvider()) + .completerInvocationProvider(new AeshCompleterInvocationProvider()) + .validatorInvocationProvider(new AeshValidatorInvocationProvider()) + .optionActivatorProvider(new AeshOptionActivatorProvider()) + .commandActivatorProvider(new AeshCommandActivatorProvider()).build()); + + @CommandDefinition(name = "test", description = "test command") + public static class TestCommand implements Command { + @Option(shortName = 'v', hasValue = false) + boolean verbose; + + @Option(defaultValue = "hello") + String greeting; + + @Option + int count; + + @Override + public CommandResult execute(CommandInvocation invocation) { + return CommandResult.SUCCESS; + } + } + + /** + * Build a ProcessedCommand manually with field setters, like the annotation processor would. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private CommandLineParser buildParserWithFieldSetters(TestCommand instance) throws Exception { + ProcessedCommand processedCommand = ((ProcessedCommandBuilder) ProcessedCommandBuilder.builder()) + .name("test") + .description("test command") + .command(instance) + .create(); + + processedCommand.addOption( + ProcessedOptionBuilder.builder() + .shortName('v') + .name("verbose") + .type(boolean.class) + .fieldName("verbose") + .optionType(OptionType.BOOLEAN) + .hasValue(false) + .fieldSetter((inst, val) -> ((TestCommand) inst).verbose = (boolean) val) + .fieldResetter(inst -> ((TestCommand) inst).verbose = false) + .build()); + + processedCommand.addOption( + ProcessedOptionBuilder.builder() + .name("greeting") + .type(String.class) + .fieldName("greeting") + .optionType(OptionType.NORMAL) + .addDefaultValue("hello") + .fieldSetter((inst, val) -> ((TestCommand) inst).greeting = (String) val) + .fieldResetter(inst -> ((TestCommand) inst).greeting = null) + .build()); + + processedCommand.addOption( + ProcessedOptionBuilder.builder() + .name("count") + .type(int.class) + .fieldName("count") + .optionType(OptionType.NORMAL) + .fieldSetter((inst, val) -> ((TestCommand) inst).count = (int) val) + .fieldResetter(inst -> ((TestCommand) inst).count = 0) + .build()); + + return CommandLineParserBuilder., CommandInvocation> builder() + .processedCommand(processedCommand) + .create(); + } + + @Test + public void testBooleanFlagWithFieldSetter() throws Exception { + TestCommand cmd = new TestCommand(); + CommandLineParser parser = buildParserWithFieldSetters(cmd); + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + parser.parse("test -v"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + + assertTrue("verbose should be true after -v", cmd.verbose); + } + + @Test + public void testBooleanFlagResetWithFieldResetter() throws Exception { + TestCommand cmd = new TestCommand(); + CommandLineParser parser = buildParserWithFieldSetters(cmd); + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + // First set it + parser.parse("test -v"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + assertTrue(cmd.verbose); + + // Then parse without -v — should reset + parser.parse("test"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + assertFalse("verbose should be reset to false", cmd.verbose); + } + + @Test + public void testStringOptionWithFieldSetter() throws Exception { + TestCommand cmd = new TestCommand(); + CommandLineParser parser = buildParserWithFieldSetters(cmd); + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + parser.parse("test --greeting=world"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + + assertEquals("world", cmd.greeting); + } + + @Test + public void testDefaultValueAppliedWithFieldSetter() throws Exception { + TestCommand cmd = new TestCommand(); + CommandLineParser parser = buildParserWithFieldSetters(cmd); + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + // Parse without --greeting — default "hello" should be applied + parser.parse("test"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + + assertEquals("hello", cmd.greeting); + } + + @Test + public void testIntOptionWithFieldSetter() throws Exception { + TestCommand cmd = new TestCommand(); + CommandLineParser parser = buildParserWithFieldSetters(cmd); + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + parser.parse("test --count=42"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + + assertEquals(42, cmd.count); + } + + @Test + public void testIntOptionResetWithFieldResetter() throws Exception { + TestCommand cmd = new TestCommand(); + CommandLineParser parser = buildParserWithFieldSetters(cmd); + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + parser.parse("test --count=42"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + assertEquals(42, cmd.count); + + // Reset + parser.parse("test"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + assertEquals(0, cmd.count); + } + + @Test + public void testStringOptionResetWithFieldResetter() throws Exception { + TestCommand cmd = new TestCommand(); + cmd.greeting = "something"; + CommandLineParser parser = buildParserWithFieldSetters(cmd); + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + // Parse with --greeting explicitly — should set + parser.parse("test --greeting=world"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + assertEquals("world", cmd.greeting); + } + + /** + * Verify that field setters from the annotation processor path work with the + * standard AeshCommandContainerBuilder (via MetadataProviderRegistry). + * This uses reflection-based path for comparison. + */ + @Test + public void testReflectionPathStillWorks() throws Exception { + // Use the standard reflection-based builder (no field setters) + TestCommand cmd = new TestCommand(); + CommandLineParser parser = new org.aesh.command.impl.container.AeshCommandContainerBuilder() + .create(cmd).getParser(); + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + parser.parse("test -v --greeting=world --count=5"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + + assertTrue(cmd.verbose); + assertEquals("world", cmd.greeting); + assertEquals(5, cmd.count); + } +} diff --git a/aesh/src/test/java/org/aesh/command/populator/MetadataProviderFieldSetterTest.java b/aesh/src/test/java/org/aesh/command/populator/MetadataProviderFieldSetterTest.java new file mode 100644 index 00000000..06d7903a --- /dev/null +++ b/aesh/src/test/java/org/aesh/command/populator/MetadataProviderFieldSetterTest.java @@ -0,0 +1,183 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.aesh.command.populator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.aesh.command.Command; +import org.aesh.command.CommandDefinition; +import org.aesh.command.CommandResult; +import org.aesh.command.impl.activator.AeshCommandActivatorProvider; +import org.aesh.command.impl.activator.AeshOptionActivatorProvider; +import org.aesh.command.impl.completer.AeshCompleterInvocationProvider; +import org.aesh.command.impl.container.AeshCommandContainerBuilder; +import org.aesh.command.impl.converter.AeshConverterInvocationProvider; +import org.aesh.command.impl.internal.OptionType; +import org.aesh.command.impl.internal.ProcessedCommand; +import org.aesh.command.impl.internal.ProcessedCommandBuilder; +import org.aesh.command.impl.internal.ProcessedOption; +import org.aesh.command.impl.internal.ProcessedOptionBuilder; +import org.aesh.command.impl.invocation.AeshInvocationProviders; +import org.aesh.command.impl.parser.CommandLineParser; +import org.aesh.command.impl.validator.AeshValidatorInvocationProvider; +import org.aesh.command.invocation.CommandInvocation; +import org.aesh.command.invocation.InvocationProviders; +import org.aesh.command.metadata.CommandMetadataProvider; +import org.aesh.command.option.Option; +import org.aesh.command.parser.CommandLineParserException; +import org.aesh.command.settings.SettingsBuilder; +import org.aesh.console.AeshContext; +import org.junit.Test; + +/** + * Tests the full flow: MetadataProvider -> AeshCommandContainerBuilder -> populator + * with field setters, simulating what happens in a real application. + */ +public class MetadataProviderFieldSetterTest { + + private final InvocationProviders invocationProviders = new AeshInvocationProviders( + SettingsBuilder.builder() + .converterInvocationProvider(new AeshConverterInvocationProvider()) + .completerInvocationProvider(new AeshCompleterInvocationProvider()) + .validatorInvocationProvider(new AeshValidatorInvocationProvider()) + .optionActivatorProvider(new AeshOptionActivatorProvider()) + .commandActivatorProvider(new AeshCommandActivatorProvider()).build()); + + @CommandDefinition(name = "mytest", description = "test") + public static class MyTestCommand implements Command { + @Option(shortName = 'v', hasValue = false) + boolean verbose; + + @Option(defaultValue = "WARN") + String level; + + @Override + public CommandResult execute(CommandInvocation invocation) { + return CommandResult.SUCCESS; + } + } + + /** + * A manually created metadata provider, equivalent to what the annotation processor generates. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static class MyTestCommand_AeshMetadata implements CommandMetadataProvider { + @Override + public Class commandType() { + return MyTestCommand.class; + } + + @Override + public MyTestCommand newInstance() { + return new MyTestCommand(); + } + + @Override + public boolean isGroupCommand() { + return false; + } + + @Override + public Class[] groupCommandClasses() { + return new Class[0]; + } + + @Override + public ProcessedCommand buildProcessedCommand(MyTestCommand instance) throws CommandLineParserException { + try { + ProcessedCommand processedCommand = ((ProcessedCommandBuilder) ProcessedCommandBuilder.builder()) + .name("mytest") + .description("test") + .command(instance) + .create(); + + processedCommand.addOption( + ProcessedOptionBuilder.builder() + .shortName('v') + .name("verbose") + .type(boolean.class) + .fieldName("verbose") + .optionType(OptionType.BOOLEAN) + .hasValue(false) + .fieldSetter((inst, val) -> ((MyTestCommand) inst).verbose = (boolean) val) + .fieldResetter(inst -> ((MyTestCommand) inst).verbose = false) + .build()); + + processedCommand.addOption( + ProcessedOptionBuilder.builder() + .name("level") + .type(String.class) + .fieldName("level") + .optionType(OptionType.NORMAL) + .addDefaultValue("WARN") + .fieldSetter((inst, val) -> ((MyTestCommand) inst).level = (String) val) + .fieldResetter(inst -> ((MyTestCommand) inst).level = null) + .build()); + + return processedCommand; + } catch (Exception e) { + throw new CommandLineParserException(e.getMessage()); + } + } + } + + @Test + public void testMetadataProviderWithFieldSetters() throws Exception { + // Manually register the provider (in real apps, ServiceLoader does this) + // We'll create the container directly via the provider + MyTestCommand cmd = new MyTestCommand(); + MyTestCommand_AeshMetadata provider = new MyTestCommand_AeshMetadata(); + + ProcessedCommand processedCommand = provider.buildProcessedCommand(cmd); + + // Verify field setters are present + assertNotNull("verbose setter should exist", + ((ProcessedOption) processedCommand.getOptions().get(0)).getFieldSetter()); + assertNotNull("level setter should exist", + ((ProcessedOption) processedCommand.getOptions().get(1)).getFieldSetter()); + } + + @Test + public void testContainerBuilderUsesMetadataProvider() throws Exception { + // Register our provider so AeshCommandContainerBuilder finds it + // We need to use ServiceLoader normally, but for testing we can build directly + MyTestCommand cmd = new MyTestCommand(); + MyTestCommand_AeshMetadata provider = new MyTestCommand_AeshMetadata(); + + ProcessedCommand processedCommand = provider.buildProcessedCommand(cmd); + + CommandLineParser parser = org.aesh.command.impl.parser.CommandLineParserBuilder + ., CommandInvocation> builder() + .processedCommand(processedCommand) + .create(); + + AeshContext aeshContext = SettingsBuilder.builder().build().aeshContext(); + + // Test boolean flag + parser.parse("mytest -v"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + assertTrue("verbose should be true", cmd.verbose); + + // Test default value + parser.parse("mytest"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + assertFalse("verbose should be reset", cmd.verbose); + assertEquals("WARN", cmd.level); + + // Test explicit value + parser.parse("mytest --level=ERROR"); + parser.getCommandPopulator().populateObject(parser.getProcessedCommand(), invocationProviders, aeshContext, + CommandLineParser.Mode.VALIDATE); + assertEquals("ERROR", cmd.level); + } +}