, extract V (index 1)
+ String valueType = getGenericTypeArgument(field.asType(), 1, typeUtils);
+
+ sb.append(" processedCommand.addOption(\n");
+ sb.append(" ProcessedOptionBuilder.builder()\n");
+ sb.append(" .shortName(").append(charLiteral(og.shortName())).append(")\n");
+ sb.append(" .name(").append(stringLiteral(og.name().length() < 1 ? fieldName : og.name())).append(")\n");
+ sb.append(" .description(").append(stringLiteral(og.description())).append(")\n");
+ sb.append(" .required(").append(og.required()).append(")\n");
+ sb.append(" .valueSeparator(',')\n");
+ sb.append(" .askIfNotSet(").append(og.askIfNotSet()).append(")\n");
+ sb.append(" .addAllDefaultValues(").append(stringArrayLiteralAsNew(og.defaultValue())).append(")\n");
+ sb.append(" .type(").append(valueType).append(".class)\n");
+ sb.append(" .fieldName(").append(stringLiteral(fieldName)).append(")\n");
+ sb.append(" .optionType(OptionType.GROUP)\n");
+ generateOptionConverter(sb, field, "converter", elementUtils);
+ generateOptionCompleter(sb, field, "completer", false, false, elementUtils);
+ generateOptionValidator(sb, field, "validator", elementUtils);
+ generateOptionActivator(sb, field, "activator", elementUtils);
+ generateOptionRenderer(sb, field, "renderer", elementUtils);
+ generateOptionParser(sb, field, "parser", elementUtils);
+ sb.append(" .build());\n\n");
+ }
+
+ private static void generateArguments(StringBuilder sb, VariableElement field, Arguments a,
+ Elements elementUtils, Types typeUtils) {
+ String fieldName = field.getSimpleName().toString();
+ String elementType = getGenericTypeArgument(field.asType(), 0, typeUtils);
+
+ sb.append(" processedCommand.setArguments(\n");
+ sb.append(" ProcessedOptionBuilder.builder()\n");
+ sb.append(" .shortName('\\u0000')\n");
+ sb.append(" .name(\"\")\n");
+ sb.append(" .description(").append(stringLiteral(a.description())).append(")\n");
+ sb.append(" .required(").append(a.required()).append(")\n");
+ sb.append(" .valueSeparator(").append(charLiteral(a.valueSeparator())).append(")\n");
+ sb.append(" .selector(").append(selectorLiteral(a.selector())).append(")\n");
+ sb.append(" .askIfNotSet(").append(a.askIfNotSet()).append(")\n");
+ sb.append(" .addAllDefaultValues(").append(stringArrayLiteralAsNew(a.defaultValue())).append(")\n");
+ sb.append(" .type(").append(elementType).append(".class)\n");
+ sb.append(" .fieldName(").append(stringLiteral(fieldName)).append(")\n");
+ sb.append(" .optionType(OptionType.ARGUMENTS)\n");
+ generateOptionConverter(sb, field, "converter", elementUtils);
+ generateOptionCompleter(sb, field, "completer", false, false, elementUtils);
+ generateOptionValidator(sb, field, "validator", elementUtils);
+ generateOptionActivator(sb, field, "activator", elementUtils);
+ generateOptionParser(sb, field, "parser", elementUtils);
+ sb.append(" .url(").append(a.url()).append(")\n");
+ sb.append(" .build());\n\n");
+ }
+
+ private static void generateArgument(StringBuilder sb, VariableElement field, Argument arg,
+ Elements elementUtils, Types typeUtils) {
+ String fieldName = field.getSimpleName().toString();
+ String fieldType = getBoxedTypeName(field.asType(), typeUtils);
+
+ sb.append(" processedCommand.setArgument(\n");
+ sb.append(" ProcessedOptionBuilder.builder()\n");
+ sb.append(" .shortName('\\u0000')\n");
+ sb.append(" .name(\"\")\n");
+ sb.append(" .description(").append(stringLiteral(arg.description())).append(")\n");
+ sb.append(" .required(").append(arg.required()).append(")\n");
+ sb.append(" .valueSeparator(' ')\n");
+ sb.append(" .askIfNotSet(").append(arg.askIfNotSet()).append(")\n");
+ sb.append(" .selector(").append(selectorLiteral(arg.selector())).append(")\n");
+ sb.append(" .addAllDefaultValues(").append(stringArrayLiteralAsNew(arg.defaultValue())).append(")\n");
+ sb.append(" .type(").append(fieldType).append(".class)\n");
+ sb.append(" .fieldName(").append(stringLiteral(fieldName)).append(")\n");
+ sb.append(" .optionType(OptionType.ARGUMENT)\n");
+ generateOptionConverter(sb, field, "converter", elementUtils);
+ generateOptionCompleter(sb, field, "completer", false, isFileOrResourceType(field.asType(), typeUtils), elementUtils);
+ generateOptionValidator(sb, field, "validator", elementUtils);
+ generateOptionActivator(sb, field, "activator", elementUtils);
+ generateOptionRenderer(sb, field, "renderer", elementUtils);
+ generateOptionParser(sb, field, "parser", elementUtils);
+ sb.append(" .overrideRequired(").append(arg.overrideRequired()).append(")\n");
+ sb.append(" .inherited(").append(arg.inherited()).append(")\n");
+ sb.append(" .url(").append(arg.url()).append(")\n");
+ sb.append(" .build());\n\n");
+ }
+
+ // --- Helper methods for generating option component instantiation ---
+
+ private static void generateOptionConverter(StringBuilder sb, VariableElement field,
+ String attributeName, Elements elementUtils) {
+ String className = getFieldAnnotationClassValue(field, attributeName, elementUtils);
+ if (className != null && !className.equals(NULL_CONVERTER)) {
+ sb.append(" .converter(new ").append(className).append("())\n");
+ } else {
+ // Pass the Class so the builder can resolve via CLConverterManager
+ sb.append(" .converter(").append(className != null ? className : NULL_CONVERTER).append(".class)\n");
+ }
+ }
+
+ private static void generateOptionCompleter(StringBuilder sb, VariableElement field,
+ String attributeName, boolean isBooleanType, boolean isFileOrResource, Elements elementUtils) {
+ String className = getFieldAnnotationClassValue(field, attributeName, elementUtils);
+ if (className != null && !className.equals(NULL_OPTION_COMPLETER)) {
+ sb.append(" .completer(new ").append(className).append("())\n");
+ } else if (isBooleanType) {
+ sb.append(" .completer(new org.aesh.command.impl.completer.BooleanOptionCompleter())\n");
+ } else if (isFileOrResource) {
+ sb.append(" .completer(new org.aesh.command.impl.completer.FileOptionCompleter())\n");
+ } else {
+ sb.append(" .completer(").append(NULL_OPTION_COMPLETER).append(".class)\n");
+ }
+ }
+
+ private static void generateOptionValidator(StringBuilder sb, VariableElement field,
+ String attributeName, Elements elementUtils) {
+ String className = getFieldAnnotationClassValue(field, attributeName, elementUtils);
+ if (className != null && !className.equals(NULL_VALIDATOR)) {
+ sb.append(" .validator(new ").append(className).append("())\n");
+ } else {
+ sb.append(" .validator(").append(NULL_VALIDATOR).append(".class)\n");
+ }
+ }
+
+ private static void generateOptionActivator(StringBuilder sb, VariableElement field,
+ String attributeName, Elements elementUtils) {
+ String className = getFieldAnnotationClassValue(field, attributeName, elementUtils);
+ if (className != null && !className.equals(NULL_ACTIVATOR)) {
+ sb.append(" .activator(new ").append(className).append("())\n");
+ } else {
+ sb.append(" .activator(").append(NULL_ACTIVATOR).append(".class)\n");
+ }
+ }
+
+ private static void generateOptionRenderer(StringBuilder sb, VariableElement field,
+ String attributeName, Elements elementUtils) {
+ String className = getFieldAnnotationClassValue(field, attributeName, elementUtils);
+ if (className != null && !className.equals(NULL_OPTION_RENDERER)) {
+ sb.append(" .renderer(new ").append(className).append("())\n");
+ } else {
+ sb.append(" .renderer(").append(NULL_OPTION_RENDERER).append(".class)\n");
+ }
+ }
+
+ private static void generateOptionParser(StringBuilder sb, VariableElement field,
+ String attributeName, Elements elementUtils) {
+ String className = getFieldAnnotationClassValue(field, attributeName, elementUtils);
+ if (className != null && !className.equals(AESH_OPTION_PARSER)) {
+ sb.append(" .parser(new ").append(className).append("())\n");
+ } else {
+ sb.append(" .parser(").append(AESH_OPTION_PARSER).append(".class)\n");
+ }
+ }
+
+ // --- Annotation class value extraction (handles MirroredTypeException) ---
+
+ /**
+ * Extract the Class value of a named annotation attribute from the field's
+ * option annotation. Returns the fully-qualified class name, or null if not found.
+ */
+ private static String getFieldAnnotationClassValue(VariableElement field, String attributeName,
+ Elements elementUtils) {
+ // Check which annotation is on this field and extract from it
+ String[] annotationTypes = {
+ Option.class.getCanonicalName(),
+ OptionList.class.getCanonicalName(),
+ OptionGroup.class.getCanonicalName(),
+ Arguments.class.getCanonicalName(),
+ Argument.class.getCanonicalName()
+ };
+
+ for (AnnotationMirror mirror : field.getAnnotationMirrors()) {
+ String annotationType = ((TypeElement) mirror.getAnnotationType().asElement())
+ .getQualifiedName().toString();
+ for (String expected : annotationTypes) {
+ if (annotationType.equals(expected)) {
+ return extractClassAttribute(mirror, attributeName);
+ }
+ }
+ }
+ return null;
+ }
+
+ private static String getAnnotationClassValue(TypeElement element, String annotationType,
+ String attributeName, Elements elementUtils) {
+ for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
+ String mirrorType = ((TypeElement) mirror.getAnnotationType().asElement())
+ .getQualifiedName().toString();
+ if (mirrorType.equals(annotationType)) {
+ return extractClassAttribute(mirror, attributeName);
+ }
+ }
+ return null;
+ }
+
+ private static String extractClassAttribute(AnnotationMirror mirror, String attributeName) {
+ for (java.util.Map.Entry extends ExecutableElement, ? extends AnnotationValue> entry :
+ mirror.getElementValues().entrySet()) {
+ if (entry.getKey().getSimpleName().toString().equals(attributeName)) {
+ Object value = entry.getValue().getValue();
+ if (value instanceof TypeMirror) {
+ return ((TypeMirror) value).toString();
+ }
+ }
+ }
+ return null;
+ }
+
+ // --- Type utility methods ---
+
+ private static boolean isBooleanType(TypeMirror type, Types typeUtils) {
+ if (type.getKind() == TypeKind.BOOLEAN) return true;
+ String name = type.toString();
+ return name.equals("java.lang.Boolean");
+ }
+
+ private static boolean isFileOrResourceType(TypeMirror type, Types typeUtils) {
+ String name = type.toString();
+ return name.equals("java.io.File") || name.equals("org.aesh.io.Resource");
+ }
+
+ private static String getBoxedTypeName(TypeMirror type, Types typeUtils) {
+ switch (type.getKind()) {
+ case BOOLEAN:
+ return "boolean";
+ case BYTE:
+ return "byte";
+ case SHORT:
+ return "short";
+ case INT:
+ return "int";
+ case LONG:
+ return "long";
+ case FLOAT:
+ return "float";
+ case DOUBLE:
+ return "double";
+ case CHAR:
+ return "char";
+ default:
+ return type.toString();
+ }
+ }
+
+ private static String getGenericTypeArgument(TypeMirror type, int index, Types typeUtils) {
+ if (type instanceof DeclaredType) {
+ DeclaredType declaredType = (DeclaredType) type;
+ List extends TypeMirror> typeArgs = declaredType.getTypeArguments();
+ if (typeArgs.size() > index) {
+ return typeArgs.get(index).toString();
+ }
+ }
+ return "Object";
+ }
+
+ // --- String literal helpers ---
+
+ private static String stringLiteral(String value) {
+ if (value == null) return "null";
+ return "\"" + escapeJavaString(value) + "\"";
+ }
+
+ private static String charLiteral(char value) {
+ if (value == '\u0000') return "'\\u0000'";
+ if (value == '\'') return "'\\''";
+ if (value == '\\') return "'\\\\'";
+ if (value == '\n') return "'\\n'";
+ if (value == '\r') return "'\\r'";
+ if (value == '\t') return "'\\t'";
+ return "'" + value + "'";
+ }
+
+ private static String stringArrayLiteral(String[] values) {
+ if (values == null || values.length == 0) return "";
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < values.length; i++) {
+ if (i > 0) sb.append(", ");
+ sb.append(stringLiteral(values[i]));
+ }
+ return sb.toString();
+ }
+
+ private static String stringArrayLiteralAsNew(String[] values) {
+ if (values == null || values.length == 0) {
+ return "new String[0]";
+ }
+ StringBuilder sb = new StringBuilder("new String[] {");
+ for (int i = 0; i < values.length; i++) {
+ if (i > 0) sb.append(", ");
+ sb.append(stringLiteral(values[i]));
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ private static String selectorLiteral(org.aesh.selector.SelectorType selectorType) {
+ return "org.aesh.selector.SelectorType." + selectorType.name();
+ }
+
+ private static String escapeJavaString(String s) {
+ StringBuilder sb = new StringBuilder();
+ for (char c : s.toCharArray()) {
+ switch (c) {
+ case '"':
+ sb.append("\\\"");
+ break;
+ case '\\':
+ sb.append("\\\\");
+ break;
+ case '\n':
+ sb.append("\\n");
+ break;
+ case '\r':
+ sb.append("\\r");
+ break;
+ case '\t':
+ sb.append("\\t");
+ break;
+ default:
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/aesh-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/aesh-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 00000000..0c8d841b
--- /dev/null
+++ b/aesh-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+org.aesh.processor.AeshAnnotationProcessor
diff --git a/aesh-processor/src/test/java/org/aesh/processor/ProcessorBenchmarkTest.java b/aesh-processor/src/test/java/org/aesh/processor/ProcessorBenchmarkTest.java
new file mode 100644
index 00000000..5c423b68
--- /dev/null
+++ b/aesh-processor/src/test/java/org/aesh/processor/ProcessorBenchmarkTest.java
@@ -0,0 +1,323 @@
+/*
+ * JBoss, Home of Professional Open Source
+ * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @authors tag. All rights reserved.
+ *
+ * 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
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.aesh.processor;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+import org.aesh.command.Command;
+import org.aesh.command.container.CommandContainer;
+import org.aesh.command.impl.container.AeshCommandContainer;
+import org.aesh.command.impl.container.AeshCommandContainerBuilder;
+import org.aesh.command.impl.internal.ProcessedCommand;
+import org.aesh.command.impl.parser.CommandLineParserBuilder;
+import org.aesh.command.metadata.CommandMetadataProvider;
+import org.junit.Test;
+
+/**
+ * Benchmark comparing reflection-based vs generated metadata command creation.
+ *
+ * Not a JMH benchmark (no forking, no blackhole sinks), but provides a
+ * consistent directional comparison. Results are written to stdout and
+ * captured in surefire reports.
+ *
+ * @author Aesh team
+ */
+public class ProcessorBenchmarkTest {
+
+ private static final int WARMUP_ITERATIONS = 2000;
+ private static final int MEASURED_ITERATIONS = 5000;
+
+ // A command with 14 annotated fields: Options, OptionList, OptionGroup, Argument
+ private static final String RICH_COMMAND_SOURCE =
+ "package bench;\n" +
+ "\n" +
+ "import java.util.List;\n" +
+ "import java.util.Map;\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Argument;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "import org.aesh.command.option.OptionList;\n" +
+ "import org.aesh.command.option.OptionGroup;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"rich\", description = \"A command with many options\",\n" +
+ " aliases = {\"r\", \"rc\"})\n" +
+ "public class RichCommand implements Command {\n" +
+ " @Option(shortName = 'V', hasValue = false, description = \"Verbose\")\n" +
+ " private boolean verbose;\n" +
+ "\n" +
+ " @Option(shortName = 'q', hasValue = false, description = \"Quiet\")\n" +
+ " private boolean quiet;\n" +
+ "\n" +
+ " @Option(shortName = 'o', description = \"Output file\", required = true)\n" +
+ " private String output;\n" +
+ "\n" +
+ " @Option(shortName = 'f', description = \"Format\")\n" +
+ " private String format;\n" +
+ "\n" +
+ " @Option(description = \"Timeout in seconds\", defaultValue = \"30\")\n" +
+ " private int timeout;\n" +
+ "\n" +
+ " @Option(description = \"Max retries\", defaultValue = \"3\")\n" +
+ " private int maxRetries;\n" +
+ "\n" +
+ " @Option(description = \"Enable compression\", hasValue = false)\n" +
+ " private boolean compress;\n" +
+ "\n" +
+ " @Option(description = \"Encoding\")\n" +
+ " private String encoding;\n" +
+ "\n" +
+ " @Option(description = \"Log level\", defaultValue = \"INFO\")\n" +
+ " private String logLevel;\n" +
+ "\n" +
+ " @Option(description = \"Config file\")\n" +
+ " private String config;\n" +
+ "\n" +
+ " @OptionList(shortName = 'i', description = \"Include patterns\")\n" +
+ " private List includes;\n" +
+ "\n" +
+ " @OptionList(shortName = 'e', description = \"Exclude patterns\")\n" +
+ " private List excludes;\n" +
+ "\n" +
+ " @OptionGroup(shortName = 'D', description = \"System properties\")\n" +
+ " private Map properties;\n" +
+ "\n" +
+ " @Argument(description = \"Source directory\")\n" +
+ " private String source;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation ci) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ // A minimal command with a single option
+ private static final String SIMPLE_COMMAND_SOURCE =
+ "package bench;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"simple\", description = \"Simple command\")\n" +
+ "public class SimpleCommand implements Command {\n" +
+ " @Option(shortName = 'n', description = \"Name\")\n" +
+ " private String name;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation ci) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void benchmarkReflectionVsGenerated() throws Exception {
+ CompilationResult result = compileWithProcessor(
+ new InMemorySource("bench.RichCommand", RICH_COMMAND_SOURCE),
+ new InMemorySource("bench.SimpleCommand", SIMPLE_COMMAND_SOURCE));
+
+ if (!result.success) {
+ System.out.println("Compilation failed: " + result.diagnostics);
+ return;
+ }
+
+ Class> richClass = result.classLoader.loadClass("bench.RichCommand");
+ Class> simpleClass = result.classLoader.loadClass("bench.SimpleCommand");
+ CommandMetadataProvider richProvider =
+ (CommandMetadataProvider) result.classLoader.loadClass("bench.RichCommand_AeshMetadata").newInstance();
+ CommandMetadataProvider simpleProvider =
+ (CommandMetadataProvider) result.classLoader.loadClass("bench.SimpleCommand_AeshMetadata").newInstance();
+
+ System.out.println("=== Benchmark: Rich command (14 annotated fields) ===");
+ System.out.println("Warmup: " + WARMUP_ITERATIONS + ", Measured: " + MEASURED_ITERATIONS + " iterations");
+ System.out.println();
+ benchmarkPair("Rich command", richClass, richProvider);
+
+ System.out.println();
+ System.out.println("=== Benchmark: Simple command (1 annotated field) ===");
+ benchmarkPair("Simple command", simpleClass, simpleProvider);
+
+ System.out.println();
+ System.out.println("=== First-call latency (single invocation, JVM already warm) ===");
+ long singleReflNs = timeOnce(() -> createViaReflection(richClass));
+ long singleGenNs = timeOnce(() -> createViaProvider(richProvider, richClass));
+ System.out.printf(" Reflection: %,d ns%n", singleReflNs);
+ System.out.printf(" Generated: %,d ns%n", singleGenNs);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void benchmarkPair(String label, Class> commandClass, CommandMetadataProvider provider) throws Exception {
+ // Warmup + measure reflection
+ for (int i = 0; i < WARMUP_ITERATIONS; i++) {
+ createViaReflection(commandClass);
+ }
+ long reflectionNs = timeIterations(() -> createViaReflection(commandClass));
+
+ // Warmup + measure generated
+ for (int i = 0; i < WARMUP_ITERATIONS; i++) {
+ createViaProvider(provider, commandClass);
+ }
+ long generatedNs = timeIterations(() -> createViaProvider(provider, commandClass));
+
+ double reflAvgUs = (reflectionNs / (double) MEASURED_ITERATIONS) / 1000.0;
+ double genAvgUs = (generatedNs / (double) MEASURED_ITERATIONS) / 1000.0;
+ double speedup = reflAvgUs / genAvgUs;
+
+ System.out.printf(" Reflection: %,.1f us/op (total: %,d ms)%n", reflAvgUs, reflectionNs / 1_000_000);
+ System.out.printf(" Generated: %,.1f us/op (total: %,d ms)%n", genAvgUs, generatedNs / 1_000_000);
+ System.out.printf(" Speedup: %.2fx%n", speedup);
+ }
+
+ private long timeIterations(ThrowingRunnable runnable) throws Exception {
+ long start = System.nanoTime();
+ for (int i = 0; i < MEASURED_ITERATIONS; i++) {
+ runnable.run();
+ }
+ return System.nanoTime() - start;
+ }
+
+ private long timeOnce(ThrowingRunnable runnable) throws Exception {
+ long start = System.nanoTime();
+ runnable.run();
+ return System.nanoTime() - start;
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private CommandContainer createViaReflection(Class> commandClass) throws Exception {
+ AeshCommandContainerBuilder builder = new AeshCommandContainerBuilder();
+ Command instance = (Command) commandClass.newInstance();
+ return builder.create(instance);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private CommandContainer createViaProvider(CommandMetadataProvider provider, Class> commandClass) throws Exception {
+ Command instance = (Command) commandClass.newInstance();
+ ProcessedCommand pc = provider.buildProcessedCommand(instance);
+ return new AeshCommandContainer(
+ CommandLineParserBuilder.builder()
+ .processedCommand(pc)
+ .create());
+ }
+
+ @FunctionalInterface
+ private interface ThrowingRunnable {
+ void run() throws Exception;
+ }
+
+ // --- In-memory compilation infrastructure ---
+
+ private CompilationResult compileWithProcessor(InMemorySource... sources) throws IOException {
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ DiagnosticCollector diagnostics = new DiagnosticCollector<>();
+ Path outputDir = Files.createTempDirectory("aesh-benchmark");
+
+ try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) {
+ fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singletonList(outputDir.toFile()));
+
+ List cpFiles = new ArrayList<>();
+ for (String entry : System.getProperty("java.class.path").split(File.pathSeparator)) {
+ cpFiles.add(new File(entry));
+ }
+ fileManager.setLocation(StandardLocation.CLASS_PATH, cpFiles);
+
+ // Run annotation processor
+ JavaCompiler.CompilationTask procTask = compiler.getTask(
+ null, fileManager, diagnostics,
+ Arrays.asList("-proc:only", "-processor", AeshAnnotationProcessor.class.getName()),
+ null, Arrays.asList(sources));
+ boolean procSuccess = procTask.call();
+
+ // Collect generated sources
+ List allSources = new ArrayList<>(Arrays.asList(sources));
+ Files.walk(outputDir)
+ .filter(p -> p.toString().endsWith(".java"))
+ .forEach(p -> {
+ try {
+ String content = new String(Files.readAllBytes(p), StandardCharsets.UTF_8);
+ String className = outputDir.relativize(p).toString()
+ .replace(File.separatorChar, '.').replace(".java", "");
+ allSources.add(new InMemorySource(className, content));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ // Compile everything
+ DiagnosticCollector compileDiagnostics = new DiagnosticCollector<>();
+ JavaCompiler.CompilationTask compileTask = compiler.getTask(
+ null, fileManager, compileDiagnostics,
+ Arrays.asList("-proc:none"), null, allSources);
+ boolean compileSuccess = compileTask.call();
+
+ URLClassLoader classLoader = new URLClassLoader(
+ new URL[]{outputDir.toUri().toURL()}, getClass().getClassLoader());
+
+ return new CompilationResult(procSuccess && compileSuccess,
+ diagnostics.getDiagnostics().toString() + compileDiagnostics.getDiagnostics().toString(),
+ classLoader);
+ }
+ }
+
+ private static class CompilationResult {
+ final boolean success;
+ final String diagnostics;
+ final URLClassLoader classLoader;
+
+ CompilationResult(boolean success, String diagnostics, URLClassLoader classLoader) {
+ this.success = success;
+ this.diagnostics = diagnostics;
+ this.classLoader = classLoader;
+ }
+ }
+
+ private static class InMemorySource extends SimpleJavaFileObject {
+ private final String code;
+
+ InMemorySource(String className, String code) {
+ super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
+ this.code = code;
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+ return code;
+ }
+ }
+}
diff --git a/aesh-processor/src/test/java/org/aesh/processor/ProcessorTest.java b/aesh-processor/src/test/java/org/aesh/processor/ProcessorTest.java
new file mode 100644
index 00000000..6f7dabfc
--- /dev/null
+++ b/aesh-processor/src/test/java/org/aesh/processor/ProcessorTest.java
@@ -0,0 +1,582 @@
+/*
+ * JBoss, Home of Professional Open Source
+ * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @authors tag. All rights reserved.
+ * See the copyright.txt in the distribution for a
+ * full listing of individual contributors.
+ *
+ * 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
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.aesh.processor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+import org.aesh.command.Command;
+import org.aesh.command.impl.container.AeshCommandContainerBuilder;
+import org.aesh.command.impl.internal.ProcessedCommand;
+import org.aesh.command.impl.internal.ProcessedOption;
+import org.aesh.command.invocation.CommandInvocation;
+import org.aesh.command.metadata.CommandMetadataProvider;
+import org.junit.Test;
+
+/**
+ * Tests that the annotation processor generates metadata that is structurally
+ * equivalent to what the reflection-based AeshCommandContainerBuilder produces.
+ */
+public class ProcessorTest {
+
+ // --- Test: Simple command with @Option and @Argument ---
+
+ private static final String SIMPLE_COMMAND_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "import org.aesh.command.option.Argument;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"simple\", description = \"A simple test command\")\n" +
+ "public class SimpleCommand implements Command {\n" +
+ " @Option(shortName = 'v', description = \"Enable verbose output\")\n" +
+ " private boolean verbose;\n" +
+ "\n" +
+ " @Option(name = \"output\", description = \"Output file\", required = true)\n" +
+ " private String outputFile;\n" +
+ "\n" +
+ " @Argument(description = \"Source file\")\n" +
+ " private String source;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation commandInvocation) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ public void testSimpleCommand() throws Exception {
+ CompilationResult result = compileWithProcessor(
+ new InMemorySource("test.SimpleCommand", SIMPLE_COMMAND_SOURCE));
+ assertTrue("Compilation should succeed: " + result.diagnostics, result.success);
+
+ // Load both the command class and the generated metadata provider
+ Class> commandClass = result.classLoader.loadClass("test.SimpleCommand");
+ Class> metadataClass = result.classLoader.loadClass("test.SimpleCommand_AeshMetadata");
+
+ assertEquivalence(commandClass, metadataClass);
+ }
+
+ // --- Test: All option types ---
+
+ private static final String ALL_OPTIONS_COMMAND_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import java.util.List;\n" +
+ "import java.util.Map;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Arguments;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "import org.aesh.command.option.OptionGroup;\n" +
+ "import org.aesh.command.option.OptionList;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"allopts\", description = \"All option types\")\n" +
+ "public class AllOptionsCommand implements Command {\n" +
+ " @Option(shortName = 'n', description = \"Name\")\n" +
+ " private String name;\n" +
+ "\n" +
+ " @OptionList(shortName = 'i', description = \"Items\")\n" +
+ " private List items;\n" +
+ "\n" +
+ " @OptionGroup(shortName = 'p', description = \"Properties\")\n" +
+ " private Map properties;\n" +
+ "\n" +
+ " @Arguments(description = \"Files to process\")\n" +
+ " private List files;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation commandInvocation) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ public void testAllOptionTypes() throws Exception {
+ CompilationResult result = compileWithProcessor(
+ new InMemorySource("test.AllOptionsCommand", ALL_OPTIONS_COMMAND_SOURCE));
+ assertTrue("Compilation should succeed: " + result.diagnostics, result.success);
+
+ Class> commandClass = result.classLoader.loadClass("test.AllOptionsCommand");
+ Class> metadataClass = result.classLoader.loadClass("test.AllOptionsCommand_AeshMetadata");
+
+ assertEquivalence(commandClass, metadataClass);
+ }
+
+ // --- Test: Boolean option with negatable ---
+
+ private static final String BOOLEAN_COMMAND_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"boolcmd\", description = \"Boolean options test\")\n" +
+ "public class BooleanCommand implements Command {\n" +
+ " @Option(hasValue = false, description = \"Flag\")\n" +
+ " private boolean flag;\n" +
+ "\n" +
+ " @Option(hasValue = false, negatable = true, description = \"Negatable flag\")\n" +
+ " private Boolean negatableFlag;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation commandInvocation) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ public void testBooleanOptions() throws Exception {
+ CompilationResult result = compileWithProcessor(
+ new InMemorySource("test.BooleanCommand", BOOLEAN_COMMAND_SOURCE));
+ assertTrue("Compilation should succeed: " + result.diagnostics, result.success);
+
+ Class> commandClass = result.classLoader.loadClass("test.BooleanCommand");
+ Class> metadataClass = result.classLoader.loadClass("test.BooleanCommand_AeshMetadata");
+
+ assertEquivalence(commandClass, metadataClass);
+ }
+
+ // --- Test: Command with default values ---
+
+ private static final String DEFAULT_VALUES_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"defaults\", description = \"Default values test\")\n" +
+ "public class DefaultValuesCommand implements Command {\n" +
+ " @Option(defaultValue = {\"hello\", \"world\"}, description = \"Greeting\")\n" +
+ " private String greeting;\n" +
+ "\n" +
+ " @Option(defaultValue = \"42\", description = \"Count\")\n" +
+ " private int count;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation commandInvocation) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ public void testDefaultValues() throws Exception {
+ CompilationResult result = compileWithProcessor(
+ new InMemorySource("test.DefaultValuesCommand", DEFAULT_VALUES_SOURCE));
+ assertTrue("Compilation should succeed: " + result.diagnostics, result.success);
+
+ Class> commandClass = result.classLoader.loadClass("test.DefaultValuesCommand");
+ Class> metadataClass = result.classLoader.loadClass("test.DefaultValuesCommand_AeshMetadata");
+
+ assertEquivalence(commandClass, metadataClass);
+ }
+
+ // --- Test: Command with aliases and version ---
+
+ private static final String ALIASED_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"aliased\", description = \"Aliased command\", aliases = {\"al\", \"a\"}, " +
+ "version = \"1.0\", generateHelp = true)\n" +
+ "public class AliasedCommand implements Command {\n" +
+ " @Option(description = \"Option\")\n" +
+ " private String opt;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation commandInvocation) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ public void testAliasedCommand() throws Exception {
+ CompilationResult result = compileWithProcessor(
+ new InMemorySource("test.AliasedCommand", ALIASED_SOURCE));
+ assertTrue("Compilation should succeed: " + result.diagnostics, result.success);
+
+ Class> commandClass = result.classLoader.loadClass("test.AliasedCommand");
+ Class> metadataClass = result.classLoader.loadClass("test.AliasedCommand_AeshMetadata");
+
+ assertEquivalence(commandClass, metadataClass);
+ }
+
+ // --- Test: Class hierarchy with options in superclass ---
+
+ private static final String BASE_COMMAND_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "\n" +
+ "public abstract class BaseCommand implements Command {\n" +
+ " @Option(description = \"Debug mode\", hasValue = false)\n" +
+ " private boolean debug;\n" +
+ "}\n";
+
+ private static final String CHILD_COMMAND_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"child\", description = \"Child command\")\n" +
+ "public class ChildCommand extends BaseCommand {\n" +
+ " @Option(description = \"Name\")\n" +
+ " private String name;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation commandInvocation) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ public void testClassHierarchy() throws Exception {
+ CompilationResult result = compileWithProcessor(
+ new InMemorySource("test.BaseCommand", BASE_COMMAND_SOURCE),
+ new InMemorySource("test.ChildCommand", CHILD_COMMAND_SOURCE));
+ assertTrue("Compilation should succeed: " + result.diagnostics, result.success);
+
+ Class> commandClass = result.classLoader.loadClass("test.ChildCommand");
+ Class> metadataClass = result.classLoader.loadClass("test.ChildCommand_AeshMetadata");
+
+ assertEquivalence(commandClass, metadataClass);
+ }
+
+ // --- Test: Group command ---
+
+ private static final String SUB_COMMAND1_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"sub1\", description = \"Subcommand 1\")\n" +
+ "public class SubCommand1 implements Command {\n" +
+ " @Option(description = \"Value\")\n" +
+ " private String value;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation commandInvocation) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ private static final String SUB_COMMAND2_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"sub2\", description = \"Subcommand 2\")\n" +
+ "public class SubCommand2 implements Command {\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation commandInvocation) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ private static final String GROUP_COMMAND_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.GroupCommandDefinition;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "import org.aesh.command.option.Option;\n" +
+ "\n" +
+ "@GroupCommandDefinition(name = \"group\", description = \"Group command\",\n" +
+ " groupCommands = {SubCommand1.class, SubCommand2.class})\n" +
+ "public class GroupTestCommand implements Command {\n" +
+ " @Option(description = \"Shared option\")\n" +
+ " private String shared;\n" +
+ "\n" +
+ " @Override\n" +
+ " public CommandResult execute(CommandInvocation commandInvocation) {\n" +
+ " return CommandResult.SUCCESS;\n" +
+ " }\n" +
+ "}\n";
+
+ @Test
+ public void testGroupCommand() throws Exception {
+ CompilationResult result = compileWithProcessor(
+ new InMemorySource("test.SubCommand1", SUB_COMMAND1_SOURCE),
+ new InMemorySource("test.SubCommand2", SUB_COMMAND2_SOURCE),
+ new InMemorySource("test.GroupTestCommand", GROUP_COMMAND_SOURCE));
+ assertTrue("Compilation should succeed: " + result.diagnostics, result.success);
+
+ Class> commandClass = result.classLoader.loadClass("test.GroupTestCommand");
+ Class> metadataClass = result.classLoader.loadClass("test.GroupTestCommand_AeshMetadata");
+
+ // Verify group command metadata
+ CommandMetadataProvider provider = (CommandMetadataProvider) metadataClass.newInstance();
+ assertTrue("Should be a group command", provider.isGroupCommand());
+ assertEquals("Should have 2 subcommands", 2, provider.groupCommandClasses().length);
+
+ assertEquivalence(commandClass, metadataClass);
+ }
+
+ // --- Test: Compile-time validation catches abstract class ---
+
+ private static final String ABSTRACT_COMMAND_SOURCE =
+ "package test;\n" +
+ "\n" +
+ "import org.aesh.command.Command;\n" +
+ "import org.aesh.command.CommandDefinition;\n" +
+ "import org.aesh.command.CommandResult;\n" +
+ "import org.aesh.command.invocation.CommandInvocation;\n" +
+ "\n" +
+ "@CommandDefinition(name = \"bad\", description = \"Bad command\")\n" +
+ "public abstract class AbstractBadCommand implements Command {\n" +
+ "}\n";
+
+ @Test
+ public void testAbstractClassError() throws Exception {
+ CompilationResult result = compileWithProcessor(
+ new InMemorySource("test.AbstractBadCommand", ABSTRACT_COMMAND_SOURCE));
+ // Should have compilation errors
+ assertTrue("Should report error for abstract command class",
+ result.diagnostics.toString().contains("must not be abstract"));
+ }
+
+ // --- Equivalence assertion ---
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private void assertEquivalence(Class> commandClass, Class> metadataClass) throws Exception {
+ // Build via reflection (existing path)
+ AeshCommandContainerBuilder reflectionBuilder = new AeshCommandContainerBuilder();
+ ProcessedCommand reflectionPC = reflectionBuilder.create(
+ (Command) commandClass.newInstance()).getParser().getProcessedCommand();
+
+ // Build via generated provider
+ CommandMetadataProvider provider = (CommandMetadataProvider) metadataClass.newInstance();
+ Command instance = (Command) commandClass.newInstance();
+ ProcessedCommand generatedPC = provider.buildProcessedCommand(instance);
+
+ // Assert structural equivalence
+ assertEquals("Command name", reflectionPC.name(), generatedPC.name());
+ assertEquals("Command description", reflectionPC.description(), generatedPC.description());
+ assertEquals("Aliases", reflectionPC.getAliases(), generatedPC.getAliases());
+ assertEquals("Version",
+ reflectionPC.version() != null ? reflectionPC.version() : "",
+ generatedPC.version() != null ? generatedPC.version() : "");
+
+ // Compare options
+ List reflectionOpts = reflectionPC.getOptions();
+ List generatedOpts = generatedPC.getOptions();
+ assertEquals("Number of options", reflectionOpts.size(), generatedOpts.size());
+
+ for (int i = 0; i < reflectionOpts.size(); i++) {
+ ProcessedOption rOpt = reflectionOpts.get(i);
+ // Find matching option by name in generated (order may differ)
+ ProcessedOption gOpt = findOptionByName(generatedOpts, rOpt.name());
+ assertNotNull("Generated should have option: " + rOpt.name(), gOpt);
+
+ assertEquals("Option name", rOpt.name(), gOpt.name());
+ assertEquals("Option shortName for " + rOpt.name(), rOpt.shortName(), gOpt.shortName());
+ assertEquals("Option description for " + rOpt.name(), rOpt.description(), gOpt.description());
+ assertEquals("Option type for " + rOpt.name(), rOpt.type(), gOpt.type());
+ assertEquals("Option fieldName for " + rOpt.name(), rOpt.getFieldName(), gOpt.getFieldName());
+ assertEquals("Option optionType for " + rOpt.name(), rOpt.getOptionType(), gOpt.getOptionType());
+ assertEquals("Option required for " + rOpt.name(), rOpt.isRequired(), gOpt.isRequired());
+ assertEquals("Option defaultValues for " + rOpt.name(), rOpt.getDefaultValues(), gOpt.getDefaultValues());
+ assertEquals("Option negatable for " + rOpt.name(), rOpt.isNegatable(), gOpt.isNegatable());
+ assertEquals("Option inherited for " + rOpt.name(), rOpt.isInherited(), gOpt.isInherited());
+ }
+
+ // Compare argument
+ if (reflectionPC.getArgument() != null) {
+ assertNotNull("Generated should have argument", generatedPC.getArgument());
+ ProcessedOption rArg = reflectionPC.getArgument();
+ ProcessedOption gArg = generatedPC.getArgument();
+ assertEquals("Argument description", rArg.description(), gArg.description());
+ assertEquals("Argument type", rArg.type(), gArg.type());
+ assertEquals("Argument fieldName", rArg.getFieldName(), gArg.getFieldName());
+ assertEquals("Argument optionType", rArg.getOptionType(), gArg.getOptionType());
+ assertEquals("Argument required", rArg.isRequired(), gArg.isRequired());
+ }
+
+ // Compare arguments (plural)
+ if (reflectionPC.getArguments() != null) {
+ assertNotNull("Generated should have arguments", generatedPC.getArguments());
+ ProcessedOption rArgs = reflectionPC.getArguments();
+ ProcessedOption gArgs = generatedPC.getArguments();
+ assertEquals("Arguments description", rArgs.description(), gArgs.description());
+ assertEquals("Arguments type", rArgs.type(), gArgs.type());
+ assertEquals("Arguments fieldName", rArgs.getFieldName(), gArgs.getFieldName());
+ assertEquals("Arguments optionType", rArgs.getOptionType(), gArgs.getOptionType());
+ }
+ }
+
+ private ProcessedOption findOptionByName(List options, String name) {
+ for (ProcessedOption opt : options) {
+ if (opt.name().equals(name)) return opt;
+ }
+ return null;
+ }
+
+ // --- In-memory compilation infrastructure ---
+
+ private CompilationResult compileWithProcessor(InMemorySource... sources) throws IOException {
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ DiagnosticCollector diagnostics = new DiagnosticCollector<>();
+
+ Path outputDir = Files.createTempDirectory("aesh-processor-test");
+
+ try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) {
+ fileManager.setLocation(StandardLocation.CLASS_OUTPUT,
+ Collections.singletonList(outputDir.toFile()));
+
+ // Add aesh classes to the classpath
+ String classpath = System.getProperty("java.class.path");
+ String[] cpEntries = classpath.split(File.pathSeparator);
+ List cpFiles = new java.util.ArrayList<>();
+ for (String entry : cpEntries) {
+ cpFiles.add(new File(entry));
+ }
+ fileManager.setLocation(StandardLocation.CLASS_PATH, cpFiles);
+
+ JavaCompiler.CompilationTask task = compiler.getTask(
+ null, fileManager, diagnostics,
+ Arrays.asList("-proc:only", "-processor", AeshAnnotationProcessor.class.getName()),
+ null, Arrays.asList(sources));
+
+ boolean procSuccess = task.call();
+
+ // Now compile again with generated sources + original sources
+ // First, collect generated source files
+ List allSources = new java.util.ArrayList<>(Arrays.asList(sources));
+
+ // Find generated source files
+ collectGeneratedSources(outputDir, allSources);
+
+ // Compile everything (no annotation processing this time)
+ DiagnosticCollector compileDiagnostics = new DiagnosticCollector<>();
+ JavaCompiler.CompilationTask compileTask = compiler.getTask(
+ null, fileManager, compileDiagnostics,
+ Arrays.asList("-proc:none"),
+ null, allSources);
+
+ boolean compileSuccess = compileTask.call();
+ boolean success = procSuccess && compileSuccess;
+
+ // Build classloader from output
+ URLClassLoader classLoader = new URLClassLoader(
+ new URL[]{outputDir.toUri().toURL()},
+ getClass().getClassLoader());
+
+ String allDiags = diagnostics.getDiagnostics().toString() + " " + compileDiagnostics.getDiagnostics().toString();
+ return new CompilationResult(success, allDiags, classLoader, outputDir);
+ }
+ }
+
+ private void collectGeneratedSources(Path dir, List sources) throws IOException {
+ Files.walk(dir)
+ .filter(p -> p.toString().endsWith(".java"))
+ .forEach(p -> {
+ try {
+ String content = new String(Files.readAllBytes(p), StandardCharsets.UTF_8);
+ // Determine the class name from path
+ String relativePath = dir.relativize(p).toString();
+ String className = relativePath.replace(File.separatorChar, '.')
+ .replace(".java", "");
+ sources.add(new InMemorySource(className, content));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ private static class CompilationResult {
+ final boolean success;
+ final String diagnostics;
+ final URLClassLoader classLoader;
+ final Path outputDir;
+
+ CompilationResult(boolean success, String diagnostics, URLClassLoader classLoader, Path outputDir) {
+ this.success = success;
+ this.diagnostics = diagnostics;
+ this.classLoader = classLoader;
+ this.outputDir = outputDir;
+ }
+ }
+
+ private static class InMemorySource extends SimpleJavaFileObject {
+ private final String code;
+
+ InMemorySource(String className, String code) {
+ super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension),
+ Kind.SOURCE);
+ this.code = code;
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+ return code;
+ }
+ }
+}
diff --git a/aesh/src/main/java/org/aesh/command/impl/container/AeshCommandContainerBuilder.java b/aesh/src/main/java/org/aesh/command/impl/container/AeshCommandContainerBuilder.java
index 86bd80d5..98ccc9cc 100644
--- a/aesh/src/main/java/org/aesh/command/impl/container/AeshCommandContainerBuilder.java
+++ b/aesh/src/main/java/org/aesh/command/impl/container/AeshCommandContainerBuilder.java
@@ -47,6 +47,8 @@
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.metadata.MetadataProviderRegistry;
import org.aesh.command.option.Argument;
import org.aesh.command.option.Arguments;
import org.aesh.command.option.Option;
@@ -65,14 +67,55 @@ public class AeshCommandContainerBuilder implement
@Override
public CommandContainer create(Command command) throws CommandLineParserException {
+ CommandMetadataProvider provider = MetadataProviderRegistry.getProvider(command.getClass());
+ if (provider != null) {
+ return buildFromProvider(provider, command);
+ }
return doGenerateCommandLineParser(command);
}
@Override
public CommandContainer create(Class extends Command> command) throws CommandLineParserException {
+ CommandMetadataProvider provider = MetadataProviderRegistry.getProvider(command);
+ if (provider != null) {
+ return buildFromProvider(provider, provider.newInstance());
+ }
return doGenerateCommandLineParser(ReflectionUtil.newInstance(command));
}
+ private AeshCommandContainer buildFromProvider(CommandMetadataProvider provider, Command command)
+ throws CommandLineParserException {
+ ProcessedCommand, CI> processedCommand = provider.buildProcessedCommand(command);
+
+ AeshCommandContainer container = new AeshCommandContainer<>(
+ CommandLineParserBuilder., CI>builder()
+ .processedCommand(processedCommand)
+ .create());
+
+ if (provider.isGroupCommand()) {
+ if (command instanceof GroupCommand) {
+ List> commands = ((GroupCommand) command).getCommands();
+ if (commands != null) {
+ for (Command sub : commands) {
+ container.addChild(create(sub));
+ }
+ }
+ List> parsedCommands = ((GroupCommand) command).getParsedCommands();
+ if (parsedCommands != null) {
+ for (CommandContainer sub : parsedCommands) {
+ container.addChild(sub);
+ }
+ }
+ } else {
+ for (Class extends Command> groupClazz : provider.groupCommandClasses()) {
+ container.addChild(create(groupClazz));
+ }
+ }
+ }
+
+ return container;
+ }
+
private AeshCommandContainer doGenerateCommandLineParser(Command commandObject) throws CommandLineParserException {
Class> clazz = (Class>) commandObject.getClass();
CommandDefinition command = clazz.getAnnotation(CommandDefinition.class);
diff --git a/aesh/src/main/java/org/aesh/command/metadata/CommandMetadataProvider.java b/aesh/src/main/java/org/aesh/command/metadata/CommandMetadataProvider.java
new file mode 100644
index 00000000..4506ad6b
--- /dev/null
+++ b/aesh/src/main/java/org/aesh/command/metadata/CommandMetadataProvider.java
@@ -0,0 +1,68 @@
+/*
+ * JBoss, Home of Professional Open Source
+ * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @authors tag. All rights reserved.
+ * See the copyright.txt in the distribution for a
+ * full listing of individual contributors.
+ *
+ * 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
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.aesh.command.metadata;
+
+import org.aesh.command.Command;
+import org.aesh.command.impl.internal.ProcessedCommand;
+import org.aesh.command.parser.CommandLineParserException;
+
+/**
+ * Provides compile-time generated metadata for a command class,
+ * eliminating the need for runtime reflection to scan annotations.
+ *
+ * Implementations are generated by the aesh-processor annotation processor
+ * and discovered via {@link java.util.ServiceLoader}.
+ *
+ * @param the command type
+ * @author Aesh team
+ */
+public interface CommandMetadataProvider {
+
+ /**
+ * @return the command class this provider handles
+ */
+ Class commandType();
+
+ /**
+ * Create a new instance of the command without reflection.
+ *
+ * @return a new command instance
+ */
+ C newInstance();
+
+ /**
+ * Build the processed command metadata using literal values
+ * extracted at compile time, avoiding annotation scanning at runtime.
+ *
+ * @param instance the command instance to attach to the metadata
+ * @return the fully built ProcessedCommand
+ * @throws CommandLineParserException if command metadata is invalid
+ */
+ @SuppressWarnings("rawtypes")
+ ProcessedCommand buildProcessedCommand(C instance) throws CommandLineParserException;
+
+ /**
+ * @return true if this command is a group command with subcommands
+ */
+ boolean isGroupCommand();
+
+ /**
+ * @return the subcommand classes for a group command, empty array if not a group
+ */
+ Class extends Command>[] groupCommandClasses();
+}
diff --git a/aesh/src/main/java/org/aesh/command/metadata/MetadataProviderRegistry.java b/aesh/src/main/java/org/aesh/command/metadata/MetadataProviderRegistry.java
new file mode 100644
index 00000000..d176aff4
--- /dev/null
+++ b/aesh/src/main/java/org/aesh/command/metadata/MetadataProviderRegistry.java
@@ -0,0 +1,84 @@
+/*
+ * JBoss, Home of Professional Open Source
+ * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @authors tag. All rights reserved.
+ * See the copyright.txt in the distribution for a
+ * full listing of individual contributors.
+ *
+ * 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
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.aesh.command.metadata;
+
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.aesh.command.Command;
+
+/**
+ * Registry that discovers and caches {@link CommandMetadataProvider} implementations
+ * via {@link ServiceLoader}. Thread-safe with lazy initialization.
+ *
+ * @author Aesh team
+ */
+public final class MetadataProviderRegistry {
+
+ private static volatile Map, CommandMetadataProvider>> providers;
+
+ private MetadataProviderRegistry() {
+ }
+
+ /**
+ * Look up a provider for the given command class.
+ *
+ * @param commandClass the command class to look up
+ * @param the command type
+ * @return the provider, or null if no generated provider exists
+ */
+ @SuppressWarnings("unchecked")
+ public static CommandMetadataProvider getProvider(Class commandClass) {
+ return (CommandMetadataProvider) getProviders().get(commandClass);
+ }
+
+ private static Map, CommandMetadataProvider>> getProviders() {
+ Map, CommandMetadataProvider>> result = providers;
+ if (result == null) {
+ synchronized (MetadataProviderRegistry.class) {
+ result = providers;
+ if (result == null) {
+ result = loadProviders();
+ providers = result;
+ }
+ }
+ }
+ return result;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static Map, CommandMetadataProvider>> loadProviders() {
+ Map, CommandMetadataProvider>> map = new ConcurrentHashMap<>();
+ ServiceLoader loader = ServiceLoader.load(CommandMetadataProvider.class);
+ for (CommandMetadataProvider provider : loader) {
+ map.put(provider.commandType(), provider);
+ }
+ return map;
+ }
+
+ /**
+ * Reset the registry, forcing re-discovery on next access.
+ * Package-private for testing.
+ */
+ static void reset() {
+ synchronized (MetadataProviderRegistry.class) {
+ providers = null;
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index a7da1212..f47cd140 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
aesh
+ aesh-processor