Skip to content

Commit

Permalink
feat: process subcommand beans
Browse files Browse the repository at this point in the history
  • Loading branch information
mimbrero committed Sep 7, 2023
1 parent 6e8bb85 commit ec3e644
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 58 deletions.
9 changes: 8 additions & 1 deletion rimor/src/main/java/st/networkers/rimor/Rimor.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package st.networkers.rimor;

import st.networkers.rimor.bean.BeanManager;
import st.networkers.rimor.bean.BeanProcessor;
import st.networkers.rimor.command.CommandRegistry;
import st.networkers.rimor.execute.CommandExecutor;
import st.networkers.rimor.extension.ExtensionManager;
Expand All @@ -14,17 +15,19 @@
public class Rimor {

private final BeanManager beanManager;
private final BeanProcessor beanProcessor;
private final CommandRegistry commandRegistry;
private final CommandExecutor commandExecutor;
private final ExecutionContextProviderRegistry executionContextProviderRegistry;
private final ExecutionContextService executionContextService;
private final ExtensionManager extensionManager;
private final PathResolver pathResolver;

public Rimor(BeanManager beanManager, CommandRegistry commandRegistry, CommandExecutor commandExecutor,
public Rimor(BeanManager beanManager, BeanProcessor beanProcessor, CommandRegistry commandRegistry, CommandExecutor commandExecutor,
ExtensionManager extensionManager, ExecutionContextProviderRegistry executionContextProviderRegistry, PathResolver pathResolver,
ExecutionContextService executionContextService) {
this.beanManager = beanManager;
this.beanProcessor = beanProcessor;
this.commandRegistry = commandRegistry;
this.commandExecutor = commandExecutor;
this.extensionManager = extensionManager;
Expand Down Expand Up @@ -91,6 +94,10 @@ public BeanManager getBeanManager() {
return beanManager;
}

public BeanProcessor getBeanProcessor() {
return beanProcessor;
}

public CommandRegistry getCommandRegistry() {
return commandRegistry;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@

import st.networkers.rimor.bean.BeanProcessor;
import st.networkers.rimor.instruction.InstructionResolver;
import st.networkers.rimor.instruction.InstructionResolver.InstructionResolutionResults;
import st.networkers.rimor.instruction.InstructionResolver.InstructionResolution;
import st.networkers.rimor.util.ReflectionUtils;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;

/**
* BeanProcessor that resolves and registers {@link Command @Command}-annotated command definition classes.
*/
public class CommandProcessor implements BeanProcessor {

private final BeanProcessor beanProcessor;
private final CommandRegistry commandRegistry;
private final InstructionResolver instructionResolver;

public CommandProcessor(CommandRegistry commandRegistry, InstructionResolver instructionResolver) {
private final Map<Object, MappedCommand> resolvedCommands = new HashMap<>();

public CommandProcessor(BeanProcessor beanProcessor, CommandRegistry commandRegistry, InstructionResolver instructionResolver) {
this.beanProcessor = beanProcessor;
this.commandRegistry = commandRegistry;
this.instructionResolver = instructionResolver;
}
Expand All @@ -28,65 +30,86 @@ public void process(Object bean) {
if (!bean.getClass().isAnnotationPresent(Command.class) || isSubcommand(bean))
return;

MappedCommand command = this.resolve(bean);
MappedCommand command = this.resolve(bean, false);
command.getIdentifiers().forEach(identifier -> commandRegistry.register(identifier, command));
}

private boolean isSubcommand(Object bean) {
return bean.getClass().isMemberClass() && bean.getClass().getDeclaringClass().isAnnotationPresent(Command.class);
}

public MappedCommand resolve(Object bean) {
InstructionResolutionResults instructionResolutionResults = instructionResolver.resolveInstructions(bean);
Collection<MappedCommand> subcommands = this.resolveSubcommands(bean);
List<String> identifiers = this.resolveIdentifiers(bean);
public MappedCommand resolve(Object bean, boolean processBean) {
if (this.resolvedCommands.containsKey(bean)) {
return this.resolvedCommands.get(bean);
}

// TODO process subcommand beans
List<String> identifiers = this.resolveIdentifiers(bean);
InstructionResolution instructionResolution = instructionResolver.resolveInstructions(bean);
Collection<MappedCommand> subcommands = this.resolveSubcommands(bean);

return MappedCommand.builder()
MappedCommand command = MappedCommand.builder()
.identifiers(identifiers)
.mainInstruction(instructionResolutionResults.getMainInstruction())
.instructions(instructionResolutionResults.mapInstructionsByIdentifier())
.mainInstruction(instructionResolution.getMainInstruction())
.instructions(instructionResolution.mapInstructionsByIdentifier())
.subcommands(subcommands)
.create();

this.resolvedCommands.put(bean, command);

if (processBean) {
beanProcessor.process(bean);
}

return command;
}

private List<String> resolveIdentifiers(Object commandInstance) {
Command command = commandInstance.getClass().getAnnotation(Command.class);
private List<String> resolveIdentifiers(Object bean) {
Command command = bean.getClass().getAnnotation(Command.class);

List<String> identifiers = Arrays.stream(command.value())
.filter(identifier -> !identifier.isEmpty())
.distinct()
.collect(Collectors.toList());

if (identifiers.isEmpty())
throw new IllegalArgumentException("the specified identifiers for " + commandInstance.getClass().getSimpleName() + " are empty");
throw new IllegalArgumentException("the specified identifiers for " + bean.getClass().getSimpleName() + " are empty");

return identifiers;
}

private Collection<MappedCommand> resolveSubcommands(Object commandInstance) {
Collection<MappedCommand> subcommands = this.resolveDeclaredSubcommands(commandInstance);
private Collection<MappedCommand> resolveSubcommands(Object bean) {
List<MappedCommand> subcommands = new ArrayList<>();

if (bean instanceof CommandDefinition)
subcommands.addAll(this.resolveRegisteredSubcommands((CommandDefinition) bean));

if (commandInstance instanceof CommandDefinition)
subcommands.addAll(this.resolveRegisteredSubcommands((CommandDefinition) commandInstance));
subcommands.addAll(this.resolveDeclaredSubcommands(bean));

return subcommands;
}

private Collection<MappedCommand> resolveDeclaredSubcommands(Object commandInstance) {
return Arrays.stream(commandInstance.getClass().getClasses())
.filter(subcommandClass -> subcommandClass.isAnnotationPresent(Command.class))
.filter(subcommandClass -> !(commandInstance instanceof CommandDefinition)
|| !((CommandDefinition) commandInstance).getSubcommands().contains(subcommandClass))
.map(subcommandClass -> ReflectionUtils.instantiateInnerClass(commandInstance, subcommandClass))
.map(this::resolve)
private Collection<MappedCommand> resolveRegisteredSubcommands(CommandDefinition bean) {
return bean.getSubcommands().stream()
.map(subcommandBean -> this.resolve(subcommandBean, true))
.collect(Collectors.toList());
}

private Collection<MappedCommand> resolveRegisteredSubcommands(CommandDefinition commandInstance) {
return commandInstance.getSubcommands().stream()
.map(this::resolve)
// package-private for testing purposes
Collection<MappedCommand> resolveDeclaredSubcommands(Object bean) {
Collection<Class<?>> classesToIgnore = new ArrayList<>();

if (bean instanceof CommandDefinition) {
// manually registered subcommands should not be resolved here
((CommandDefinition) bean).getSubcommands().stream()
.map(Object::getClass)
.forEach(classesToIgnore::add);
}

return Arrays.stream(bean.getClass().getClasses())
.filter(subcommandClass -> subcommandClass.isAnnotationPresent(Command.class))
.filter(subcommandClass -> !classesToIgnore.contains(subcommandClass))
.map(subcommandClass -> ReflectionUtils.instantiateInnerClass(bean, subcommandClass))
.map(subcommandBean -> this.resolve(subcommandBean, true))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ public void configure(Rimor rimor) {

private void registerSupportBeanProcessors() {
BeanManager beanManager = rimor.getBeanManager();
InstructionResolver instructionResolver = new InstructionResolver(rimor.getExecutionContextService());

CommandProcessor commandProcessor = new CommandProcessor(rimor.getCommandRegistry(), new InstructionResolver(rimor.getExecutionContextService()));
CommandProcessor commandProcessor = new CommandProcessor(rimor.getBeanProcessor(), rimor.getCommandRegistry(), instructionResolver);
beanManager.registerBeanProcessor(commandProcessor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@ public InstructionResolver(ExecutionContextService executionContextService) {
this.executionContextService = executionContextService;
}

public InstructionResolutionResults resolveInstructions(Object bean) {
InstructionResolutionResults instructionResolutionResults = new InstructionResolutionResults();
public InstructionResolution resolveInstructions(Object bean) {
InstructionResolution instructionResolution = new InstructionResolution();

for (Method method : bean.getClass().getMethods()) {
if (!method.isAnnotationPresent(MainInstructionMapping.class) && !method.isAnnotationPresent(InstructionMapping.class))
continue;

HandlerMethodInstruction instruction = this.resolveInstruction(bean, method);
if (method.isAnnotationPresent(MainInstructionMapping.class))
instructionResolutionResults.setMainInstruction(instruction);
instructionResolution.setMainInstruction(instruction);

if (method.isAnnotationPresent(InstructionMapping.class))
instructionResolutionResults.addInstruction(instruction);
instructionResolution.addInstruction(instruction);
}
return instructionResolutionResults;
return instructionResolution;
}

public HandlerMethodInstruction resolveInstruction(Object bean, Method method) {
Expand Down Expand Up @@ -58,7 +58,7 @@ private List<String> resolveIdentifiers(Method method) {
return identifiers;
}

public static class InstructionResolutionResults {
public static class InstructionResolution {
private HandlerMethodInstruction mainInstruction = null;
private final Collection<HandlerMethodInstruction> instructions = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
package st.networkers.rimor.command;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoSettings;
import st.networkers.rimor.bean.BeanProcessor;
import st.networkers.rimor.instruction.HandlerMethodInstruction;
import st.networkers.rimor.instruction.InstructionMapping;
import st.networkers.rimor.instruction.InstructionResolver;
import st.networkers.rimor.instruction.MainInstructionMapping;
import st.networkers.rimor.reflect.CachedMethod;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

@MockitoSettings
@SuppressWarnings("InnerClassMayBeStatic")
class CommandProcessorTest {

InstructionResolver instructionResolver = new InstructionResolver(null);

@Mock CommandRegistry commandRegistry;
@Spy CommandProcessor commandProcessor = new CommandProcessor(commandRegistry, instructionResolver);
@Mock BeanProcessor beanProcessor;
CommandProcessor commandProcessor;

@BeforeEach
void beforeAll() {
commandProcessor = spy(new CommandProcessor(beanProcessor, commandRegistry, new InstructionResolver(null)));
}

@Command({"foo", "bar"})
static class TwoIdentifiersCommand {
}

@Test
void whenResolvingCommand_identifiersAreFooAndBarInOrder() {
MappedCommand command = commandProcessor.resolve(new TwoIdentifiersCommand());
MappedCommand command = commandProcessor.resolve(new TwoIdentifiersCommand(), false);
assertThat(command.getIdentifiers()).containsExactly("foo", "bar");
}

Expand All @@ -39,7 +45,7 @@ static class NoIdentifierCommand {}

@Test
void whenResolvingNoIdentifierCommand_throwsIllegalArgumentException() {
assertThatThrownBy(() -> commandProcessor.resolve(new NoIdentifierCommand())).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> commandProcessor.resolve(new NoIdentifierCommand(), false)).isInstanceOf(IllegalArgumentException.class);
}

@Command("foo")
Expand All @@ -51,7 +57,7 @@ public void defaultInstruction() {

@Test
void whenResolvingCommandWithDefaultInstruction_mainInstructionIsResolved() throws NoSuchMethodException {
MappedCommand command = commandProcessor.resolve(new CommandWithDefaultInstruction());
MappedCommand command = commandProcessor.resolve(new CommandWithDefaultInstruction(), false);
assertThat(command.getMainInstruction())
.map(instruction -> (HandlerMethodInstruction) instruction)
.map(HandlerMethodInstruction::getMethod)
Expand All @@ -68,7 +74,7 @@ public void barInstruction() {

@Test
void whenResolvingCommandWithInstruction_barInstructionIsResolved() throws NoSuchMethodException {
MappedCommand command = commandProcessor.resolve(new CommandWithInstruction());
MappedCommand command = commandProcessor.resolve(new CommandWithInstruction(), false);
assertThat(command.getInstruction("bar"))
.map(instruction -> (HandlerMethodInstruction) instruction)
.map(HandlerMethodInstruction::getMethod)
Expand All @@ -88,9 +94,9 @@ public void defaultInstruction() {

@Test
void whenResolvingCommandWithDeclaredStaticSubcommand_barSubcommandIsResolvedAndRegistered() {
MappedCommand command = commandProcessor.resolve(new CommandWithDeclaredStaticSubcommand());
MappedCommand command = commandProcessor.resolve(new CommandWithDeclaredStaticSubcommand(), false);

verify(commandProcessor).resolve(any(CommandWithDeclaredStaticSubcommand.BarSubcommand.class));
verify(commandProcessor).resolve(any(CommandWithDeclaredStaticSubcommand.BarSubcommand.class), eq(true));
assertThat(command.getSubcommand("bar")).isPresent();
}

Expand All @@ -103,9 +109,9 @@ public class BarSubcommand {

@Test
void whenResolvingCommandWithDeclaredNonStaticSubcommand_barSubcommandIsResolvedAndRegistered() {
MappedCommand command = commandProcessor.resolve(new CommandWithDeclaredNonStaticSubcommand());
MappedCommand command = commandProcessor.resolve(new CommandWithDeclaredNonStaticSubcommand(), false);

verify(commandProcessor).resolve(any(CommandWithDeclaredNonStaticSubcommand.BarSubcommand.class));
verify(commandProcessor).resolve(any(CommandWithDeclaredNonStaticSubcommand.BarSubcommand.class), eq(true));
assertThat(command.getSubcommand("bar")).isPresent();
}

Expand All @@ -116,17 +122,28 @@ public CommandWithRegisteredInnerSubcommandDefinition() {
}

@Command("bar")
static class BarSubcommand {
public static class BarSubcommand {
public BarSubcommand() {
throw new IllegalStateException("this class should not be constructed automatically!");
}

private BarSubcommand(int i) {
}
}
}

@Test
void whenResolvingCommandWithRegisteredSubcommand_barSubcommandIsResolvedAndRegistered() {
MappedCommand command = commandProcessor.resolve(new CommandWithRegisteredInnerSubcommandDefinition());
MappedCommand command = commandProcessor.resolve(new CommandWithRegisteredInnerSubcommandDefinition(), false);

verify(commandProcessor).resolve(any(CommandWithRegisteredInnerSubcommandDefinition.BarSubcommand.class));
verify(commandProcessor).resolve(any(CommandWithRegisteredInnerSubcommandDefinition.BarSubcommand.class), eq(true));
assertThat(command.getSubcommand("bar")).isPresent();
}

@Test
void whenResolvingCommandWithRegisteredInnerSubcommand_barSubcommandMustNotBeInstantiatedByProcessor() {
assertThatCode(() -> {
commandProcessor.resolveDeclaredSubcommands(new CommandWithRegisteredInnerSubcommandDefinition());
}).doesNotThrowAnyException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import st.networkers.rimor.instruction.InstructionResolver.InstructionResolutionResults;
import st.networkers.rimor.instruction.InstructionResolver.InstructionResolution;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
Expand Down Expand Up @@ -45,7 +45,7 @@ void whenResolvingBazInstruction_identifiersAreMethodName() throws NoSuchMethodE

@Test
void whenResolvingFooCommandInstructions_instructionsAreResolved() throws NoSuchMethodException {
InstructionResolutionResults instructions = instructionResolver.resolveInstructions(new FooCommand());
InstructionResolution instructions = instructionResolver.resolveInstructions(new FooCommand());
assertThat(instructions.getMainInstruction())
.isNotNull()
.extracting(instruction -> instruction.getMethod().getMethod())
Expand Down

0 comments on commit ec3e644

Please sign in to comment.