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 0ae1a0285..c77c0294a 100644 --- a/aesh-processor/src/main/java/org/aesh/processor/CodeGenerator.java +++ b/aesh-processor/src/main/java/org/aesh/processor/CodeGenerator.java @@ -244,13 +244,12 @@ static String generate( // Collect field accessor info for the switch-based Accessor class List accessorInfos = new java.util.ArrayList<>(); - // Find @ParentCommand field for the Accessor class - FieldAccessorInfo parentInfo = null; + // Collect @ParentCommand fields for type-directed ancestor injection. + List parentInfos = new java.util.ArrayList<>(); for (VariableElement field : fields) { if (field.getAnnotation(ParentCommand.class) != null) { - parentInfo = new FieldAccessorInfo(-1, field.getSimpleName().toString(), - erasedTypeName(field.asType(), typeUtils), null, isPrivateField(field), simpleName); - break; + parentInfos.add(new FieldAccessorInfo(-1, field.getSimpleName().toString(), + erasedTypeName(field.asType(), typeUtils), null, isPrivateField(field), simpleName)); } } @@ -271,7 +270,7 @@ static String generate( sb.append(" }\n\n"); // Generate the single Accessor inner class - generateAccessorClass(sb, simpleName, accessorInfos, parentInfo); + generateAccessorClass(sb, simpleName, accessorInfos, parentInfos); // End class sb.append("}\n"); @@ -1146,9 +1145,9 @@ private static void generateArgument(StringBuilder sb, String simpleName, Variab } private static void generateAccessorClass(StringBuilder sb, String commandSimpleName, - List infos, FieldAccessorInfo parentInfo) { + List infos, List parentInfos) { sb.append(" static final class Accessor implements org.aesh.command.impl.internal.FieldAccessor,\n"); - sb.append(" java.util.function.BiConsumer {\n"); + sb.append(" org.aesh.command.impl.internal.ParentCommandInjector {\n"); sb.append(" static final Accessor PARENT_INJECTOR = new Accessor(-1);\n\n"); @@ -1164,7 +1163,7 @@ private static void generateAccessorClass(StringBuilder sb, String commandSimple // set method sb.append(" public void set(Object inst, Object val) {\n"); - boolean setNeedsTry = hasPrivateFields(infos) || (parentInfo != null && parentInfo.isPrivate); + boolean setNeedsTry = hasPrivateFields(infos); if (setNeedsTry) { sb.append(" try {\n"); } @@ -1243,18 +1242,26 @@ private static void generateAccessorClass(StringBuilder sb, String commandSimple } sb.append(" }\n\n"); - // accept method (BiConsumer for parent command injection) + // Legacy direct-parent form, plus the type-directed runtime form. sb.append(" @Override\n"); sb.append(" public void accept(Object cmd, Object parent) {\n"); - if (parentInfo != null) { + sb.append(" inject(cmd, type -> type.isInstance(parent) ? parent : null);\n"); + sb.append(" }\n\n"); + sb.append(" @Override\n"); + sb.append(" public void inject(Object cmd, java.util.function.Function, Object> parentLookup) {\n"); + for (FieldAccessorInfo parentInfo : parentInfos) { + sb.append(" {").append(parentInfo.fieldType).append(" parent = (") + .append(parentInfo.fieldType).append(") parentLookup.apply(") + .append(parentInfo.fieldType).append(".class);\n"); + sb.append(" if (parent != null) {"); if (parentInfo.isPrivate) { - sb.append(" try { ").append(fieldConstantRef(fieldConstantName(parentInfo.fieldName))) - .append(".set(cmd, parent);") - .append(" } catch (IllegalAccessException e) { throw new RuntimeException(e); }\n"); + sb.append(" try { ").append(fieldConstantRef(fieldConstantName(parentInfo.fieldName))) + .append(".set(cmd, parent); } catch (IllegalAccessException e) { throw new RuntimeException(e); }"); } else { - sb.append(" ((").append(commandSimpleName).append(") cmd).").append(parentInfo.fieldName) - .append(" = (").append(parentInfo.fieldType).append(") parent;\n"); + sb.append(" ((").append(commandSimpleName).append(") cmd).") + .append(parentInfo.fieldName).append(" = parent;"); } + sb.append(" }\n }\n"); } sb.append(" }\n"); sb.append(" }\n"); @@ -2275,7 +2282,7 @@ static String generateMethodProvider( sb.append(" }\n\n"); generateMethodAdapter(sb, ownerTypeRef, method, params, elementUtils, typeUtils); - generateAccessorClass(sb, "Adapter", accessorInfos, null); + generateAccessorClass(sb, "Adapter", accessorInfos, java.util.Collections.emptyList()); sb.append("}\n"); diff --git a/aesh-processor/src/test/java/org/aesh/processor/ProcessorTest.java b/aesh-processor/src/test/java/org/aesh/processor/ProcessorTest.java index a6ad28ef7..47e9f4aa6 100644 --- a/aesh-processor/src/test/java/org/aesh/processor/ProcessorTest.java +++ b/aesh-processor/src/test/java/org/aesh/processor/ProcessorTest.java @@ -1713,6 +1713,19 @@ public void testTypeUseAnnotationsOnOptions() throws Exception { // --- Test: @ParentCommand injection in generated code --- + private static final String PARENT_ROOT_CMD_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 = \"root\", description = \"Root\")\n" + + "public class ParentRootCommand implements Command {\n" + + " @Override\n" + + " public CommandResult execute(CommandInvocation ci) { return CommandResult.SUCCESS; }\n" + + "}\n"; + private static final String PARENT_CMD_GROUP_SOURCE = "package test;\n" + "\n" + "import org.aesh.command.Command;\n" + @@ -1748,6 +1761,12 @@ public void testTypeUseAnnotationsOnOptions() throws Exception { " @ParentCommand\n" + " private ParentGroupCommand parent;\n" + "\n" + + " @ParentCommand\n" + + " private ParentGroupCommand secondParent;\n" + + "\n" + + " @ParentCommand\n" + + " private ParentRootCommand root;\n" + + "\n" + " @Option(description = \"Name\")\n" + " private String name;\n" + "\n" + @@ -1761,7 +1780,8 @@ public void testTypeUseAnnotationsOnOptions() throws Exception { public void testParentCommandInjection() throws Exception { CompilationResult result = compileWithProcessor( new InMemorySource("test.ParentChildCommand", PARENT_CHILD_CMD_SOURCE), - new InMemorySource("test.ParentGroupCommand", PARENT_CMD_GROUP_SOURCE)); + new InMemorySource("test.ParentGroupCommand", PARENT_CMD_GROUP_SOURCE), + new InMemorySource("test.ParentRootCommand", PARENT_ROOT_CMD_SOURCE)); assertTrue("Compilation should succeed: " + result.diagnostics, result.success); // Verify the child's generated metadata has a parentCommandInjector @@ -1776,12 +1796,24 @@ public void testParentCommandInjection() throws Exception { // Verify the injector works: create a parent, inject into child Class parentClass = result.classLoader.loadClass("test.ParentGroupCommand"); Object parentInstance = parentClass.newInstance(); - childPC.getParentCommandInjector().accept(childInstance, parentInstance); + Class rootClass = result.classLoader.loadClass("test.ParentRootCommand"); + Object rootInstance = rootClass.newInstance(); + childPC.getParentCommandInjector().inject(childInstance, type -> type == parentClass + ? parentInstance + : type == rootClass ? rootInstance : null); // Verify the @ParentCommand field was set via reflection java.lang.reflect.Field parentField = childInstance.getClass().getDeclaredField("parent"); parentField.setAccessible(true); assertEquals("Parent should be injected", parentInstance, parentField.get(childInstance)); + java.lang.reflect.Field secondParentField = childInstance.getClass().getDeclaredField("secondParent"); + secondParentField.setAccessible(true); + assertEquals("Every matching @ParentCommand field should be injected", parentInstance, + secondParentField.get(childInstance)); + java.lang.reflect.Field rootField = childInstance.getClass().getDeclaredField("root"); + rootField.setAccessible(true); + assertEquals("Ancestor fields should resolve by their declared type", rootInstance, + rootField.get(childInstance)); // Also verify equivalence with reflection path assertEquivalence( diff --git a/aesh/src/main/java/org/aesh/command/impl/Executions.java b/aesh/src/main/java/org/aesh/command/impl/Executions.java index a977a5c68..d93c05e6f 100644 --- a/aesh/src/main/java/org/aesh/command/impl/Executions.java +++ b/aesh/src/main/java/org/aesh/command/impl/Executions.java @@ -555,11 +555,13 @@ private static Operator buildOperator(OperatorType op, AeshContext context, Pipe } private static void injectParentCommands(ProcessedCommand processedCommand, CommandContext commandContext) { - java.util.function.BiConsumer injector = processedCommand.getParentCommandInjector(); + org.aesh.command.impl.internal.ParentCommandInjector injector = processedCommand.getParentCommandInjector(); if (injector != null) { - Command parent = commandContext.getParentCommand(); - if (parent != null) - injector.accept(processedCommand.getCommand(), parent); + injector.inject(processedCommand.getCommand(), type -> { + if (!Command.class.isAssignableFrom(type)) + return null; + return commandContext.getParentCommand((Class) type); + }); return; } Object command = processedCommand.getCommand(); 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 981fdbbe4..609da3565 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 @@ -511,30 +511,43 @@ private static void processCommand(ProcessedCommand processedCommand, Class claz } /** - * Wire the parent-command injector for the reflection path. Generated - * providers set their own injector; here a single hierarchy walk at - * registration time records the first @ParentCommand field, so execution - * performs one BiConsumer call with no reflection. Mirrors the - * sub-command-mode fallback in Executions: inaccessible fields are - * skipped, type mismatches fail. + * Wire the parent-command injector for the reflection path. Fields are + * collected at registration so execution performs no reflective scan; each + * field resolves the nearest ancestor matching its declared type. */ private static void wireParentCommandInjector(ProcessedCommand processedCommand, Class clazz) { + List parentFields = new ArrayList<>(); for (Class c = clazz; c != null; c = c.getSuperclass()) { for (Field field : c.getDeclaredFields()) { if (field.isAnnotationPresent(ParentCommand.class)) { if (!Modifier.isPublic(field.getModifiers())) field.setAccessible(true); - processedCommand.setParentCommandInjector((child, parent) -> { + parentFields.add(field); + } + } + } + if (parentFields.isEmpty()) + return; + processedCommand.setParentCommandInjector(new org.aesh.command.impl.internal.ParentCommandInjector() { + @Override + public void accept(Object child, Object parent) { + inject(child, type -> type.isInstance(parent) ? parent : null); + } + + @Override + public void inject(Object child, Function, Object> parentLookup) { + for (Field field : parentFields) { + Object parent = parentLookup.apply(field.getType()); + if (parent != null) { try { field.set(child, parent); } catch (IllegalAccessException e) { - // Mirror the Executions fallback: skip quietly + // Match the existing fallback: skip inaccessible fields. } - }); - return; + } } } - } + }); } private static void processCommand(ProcessedCommand processedCommand, Class clazz, boolean forHelp) diff --git a/aesh/src/main/java/org/aesh/command/impl/internal/ParentCommandInjector.java b/aesh/src/main/java/org/aesh/command/impl/internal/ParentCommandInjector.java new file mode 100644 index 000000000..e1dbdf33f --- /dev/null +++ b/aesh/src/main/java/org/aesh/command/impl/internal/ParentCommandInjector.java @@ -0,0 +1,39 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag + * 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.impl.internal; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Injects parent commands selected by the annotated field type. The legacy + * {@link #accept(Object, Object)} form remains available for callers that have + * only a single direct parent; runtime paths should use {@link #inject(Object, Function)}. + */ +public interface ParentCommandInjector extends BiConsumer { + + /** + * Inject each {@code @ParentCommand} field from the nearest matching parent. + * + * @param command child command instance + * @param parentLookup resolves an ancestor by the field's declared type + */ + void inject(Object command, Function, Object> parentLookup); +} 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 310ce6323..79d06b15c 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 @@ -26,7 +26,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -111,7 +110,7 @@ public int compare(ProcessedOption l, ProcessedOption r) { private final List aliases; private List parserExceptions; private CompleteStatus completeStatus; - private BiConsumer parentCommandInjector; + private ParentCommandInjector parentCommandInjector; private int optionDeclarationCounter; private List cachedPositionalOrder; private boolean hasInheritedOptions; @@ -502,11 +501,11 @@ public void setHelpSectionProvider(HelpSectionProvider helpSectionProvider) { this.helpSectionProvider = helpSectionProvider; } - public void setParentCommandInjector(BiConsumer injector) { + public void setParentCommandInjector(ParentCommandInjector injector) { this.parentCommandInjector = injector; } - public BiConsumer getParentCommandInjector() { + public ParentCommandInjector getParentCommandInjector() { return parentCommandInjector; } diff --git a/aesh/src/main/java/org/aesh/command/impl/parser/AeshCommandLineParser.java b/aesh/src/main/java/org/aesh/command/impl/parser/AeshCommandLineParser.java index b67989424..0ec1a662a 100644 --- a/aesh/src/main/java/org/aesh/command/impl/parser/AeshCommandLineParser.java +++ b/aesh/src/main/java/org/aesh/command/impl/parser/AeshCommandLineParser.java @@ -26,7 +26,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; import java.util.function.Function; import org.aesh.command.Command; @@ -405,7 +404,7 @@ private void populateSelectedPath(ProcessedCommand, CI> processedCom // @ParentCommand works outside sub-command mode too. The // direct parent instance is used, matching what // CommandContext.getParentCommand() would return. - injectParentCommand(child, processedCommand); + injectParentCommand(child); } else // Foreign parser implementations keep their legacy full behavior. child.doPopulate(child.getProcessedCommand(), invocationProviders, aeshContext, mode); @@ -420,20 +419,26 @@ private void populateSelectedPath(ProcessedCommand, CI> processedCom } /** - * Inject this level's command instance into the selected child's - * {@code @ParentCommand} field. Generated providers always carry an - * injector (a no-op without such a field); reflection-built commands get - * theirs wired at container-build time — either way this is a single - * call with no reflection on the execution path. Foreign parsers keep - * their existing behavior. + * Inject matching ancestors into the selected child's {@code @ParentCommand} + * fields. Generated providers and reflection-built commands resolve each + * field by type; foreign parsers keep their existing behavior. */ - private void injectParentCommand(CommandLineParser child, - ProcessedCommand, CI> processedCommand) { + private void injectParentCommand(CommandLineParser child) { if (!(child instanceof AeshCommandLineParser)) return; - BiConsumer injector = child.getProcessedCommand().getParentCommandInjector(); - if (injector != null) - injector.accept(child.getCommand(), processedCommand.getCommand()); + org.aesh.command.impl.internal.ParentCommandInjector injector = child.getProcessedCommand() + .getParentCommandInjector(); + if (injector != null) { + AeshCommandLineParser childParser = (AeshCommandLineParser) child; + injector.inject(child.getCommand(), type -> { + for (AeshCommandLineParser ancestor = childParser.parent; ancestor != null; ancestor = ancestor.parent) { + Command parentCommand = ancestor.getCommand(); + if (type.isInstance(parentCommand)) + return parentCommand; + } + return null; + }); + } } /** 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 2bacf45f1..52dd0a421 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 @@ -173,11 +173,13 @@ public void populateObject(ProcessedCommand, CI> processedCommand, private void injectParentCommands(ProcessedCommand, CI> processedCommand, CommandContext commandContext) { - java.util.function.BiConsumer injector = processedCommand.getParentCommandInjector(); + org.aesh.command.impl.internal.ParentCommandInjector injector = processedCommand.getParentCommandInjector(); if (injector != null) { - Command parent = commandContext.getParentCommand(); - if (parent != null) - injector.accept(getObject(), parent); + injector.inject(getObject(), type -> { + if (!Command.class.isAssignableFrom(type)) + return null; + return commandContext.getParentCommand((Class) type); + }); return; } List fields = getAllFields(getObject().getClass()); diff --git a/aesh/src/test/java/org/aesh/command/GroupParentInjectionTest.java b/aesh/src/test/java/org/aesh/command/GroupParentInjectionTest.java index ae1cc9995..7e66a638f 100644 --- a/aesh/src/test/java/org/aesh/command/GroupParentInjectionTest.java +++ b/aesh/src/test/java/org/aesh/command/GroupParentInjectionTest.java @@ -146,13 +146,18 @@ public static class Leaf implements Command { @ParentCommand Mid parent; + @ParentCommand + Root root; + static Class seenParentClass; static String seenRootValue; + static String seenAncestorValue; @Override public CommandResult execute(CommandInvocation invocation) { seenParentClass = parent.getClass(); seenRootValue = parent.parent.connectionString; + seenAncestorValue = root != null ? root.connectionString : null; return CommandResult.SUCCESS; } } @@ -169,6 +174,7 @@ public void testNestedGroupsInjectDirectParent() throws Exception { assertEquals(CommandResult.SUCCESS, runtime.executeCommand("root --c conn mid leaf")); assertEquals("leaf must link to its direct parent", Mid.class, Leaf.seenParentClass); assertEquals("values must flow through the whole chain", "conn", Leaf.seenRootValue); + assertEquals("@ParentCommand resolves an ancestor by the field's type", "conn", Leaf.seenAncestorValue); assertTrue("mid-level afterParse must run", Mid.afterParseRan); assertSame("mid must receive the runtime context", ctx, Mid.ctxAtAfterParse); }