Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions aesh-processor/src/main/java/org/aesh/processor/CodeGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,12 @@ static String generate(
// Collect field accessor info for the switch-based Accessor class
List<FieldAccessorInfo> accessorInfos = new java.util.ArrayList<>();

// Find @ParentCommand field for the Accessor class
FieldAccessorInfo parentInfo = null;
// Collect @ParentCommand fields for type-directed ancestor injection.
List<FieldAccessorInfo> 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));
}
}

Expand All @@ -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");
Expand Down Expand Up @@ -1146,9 +1145,9 @@ private static void generateArgument(StringBuilder sb, String simpleName, Variab
}

private static void generateAccessorClass(StringBuilder sb, String commandSimpleName,
List<FieldAccessorInfo> infos, FieldAccessorInfo parentInfo) {
List<FieldAccessorInfo> infos, List<FieldAccessorInfo> parentInfos) {
sb.append(" static final class Accessor implements org.aesh.command.impl.internal.FieldAccessor,\n");
sb.append(" java.util.function.BiConsumer<Object, Object> {\n");
sb.append(" org.aesh.command.impl.internal.ParentCommandInjector {\n");

sb.append(" static final Accessor PARENT_INJECTOR = new Accessor(-1);\n\n");

Expand All @@ -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");
}
Expand Down Expand Up @@ -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<Class<?>, 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");
Expand Down Expand Up @@ -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");

Expand Down
36 changes: 34 additions & 2 deletions aesh-processor/src/test/java/org/aesh/processor/ProcessorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommandInvocation> {\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" +
Expand Down Expand Up @@ -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" +
Expand All @@ -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
Expand All @@ -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(
Expand Down
10 changes: 6 additions & 4 deletions aesh/src/main/java/org/aesh/command/impl/Executions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object, Object> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Field> 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<Class<?>, 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Object, Object> {

/**
* 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<Class<?>, Object> parentLookup);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -111,7 +110,7 @@ public int compare(ProcessedOption l, ProcessedOption r) {
private final List<String> aliases;
private List<CommandLineParserException> parserExceptions;
private CompleteStatus completeStatus;
private BiConsumer<Object, Object> parentCommandInjector;
private ParentCommandInjector parentCommandInjector;
private int optionDeclarationCounter;
private List<ProcessedOption> cachedPositionalOrder;
private boolean hasInheritedOptions;
Expand Down Expand Up @@ -502,11 +501,11 @@ public void setHelpSectionProvider(HelpSectionProvider helpSectionProvider) {
this.helpSectionProvider = helpSectionProvider;
}

public void setParentCommandInjector(BiConsumer<Object, Object> injector) {
public void setParentCommandInjector(ParentCommandInjector injector) {
this.parentCommandInjector = injector;
}

public BiConsumer<Object, Object> getParentCommandInjector() {
public ParentCommandInjector getParentCommandInjector() {
return parentCommandInjector;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -405,7 +404,7 @@ private void populateSelectedPath(ProcessedCommand<Command<CI>, 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);
Expand All @@ -420,20 +419,26 @@ private void populateSelectedPath(ProcessedCommand<Command<CI>, 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<CI> child,
ProcessedCommand<Command<CI>, CI> processedCommand) {
private void injectParentCommand(CommandLineParser<CI> child) {
if (!(child instanceof AeshCommandLineParser))
return;
BiConsumer<Object, Object> 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<CI> childParser = (AeshCommandLineParser<CI>) child;
injector.inject(child.getCommand(), type -> {
for (AeshCommandLineParser<CI> ancestor = childParser.parent; ancestor != null; ancestor = ancestor.parent) {
Command<CI> parentCommand = ancestor.getCommand();
if (type.isInstance(parentCommand))
return parentCommand;
}
return null;
});
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,13 @@ public void populateObject(ProcessedCommand<Command<CI>, CI> processedCommand,

private void injectParentCommands(ProcessedCommand<Command<CI>, CI> processedCommand,
CommandContext commandContext) {
java.util.function.BiConsumer<Object, Object> 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<Field> fields = getAllFields(getObject().getClass());
Expand Down
Loading
Loading