Skip to content

Commit

Permalink
Merge pull request #5 from networkers-group/annotation-based-context-…
Browse files Browse the repository at this point in the history
…provider

Add annotation-based execution context providers
  • Loading branch information
mimbrero authored Oct 17, 2023
2 parents 1c91b10 + 1839f85 commit 8bca055
Show file tree
Hide file tree
Showing 20 changed files with 705 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package st.networkers.rimor.bean;

/**
* Signals that something went wrong while processing a bean.
*/
public class BeanProcessingException extends RuntimeException {

private final Object bean;

public BeanProcessingException(Object bean) {
this(bean, "there was an error while processing " + bean.getClass().getName());
}

public BeanProcessingException(Object bean, String message) {
super(message);
this.bean = bean;
}

public BeanProcessingException(Object bean, String message, Throwable cause) {
super(message, cause);
this.bean = bean;
}

public BeanProcessingException(Object bean, Throwable cause) {
this(bean, "there was an error while processing " + bean.getClass().getName(), cause);
}

public Object getBean() {
return bean;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package st.networkers.rimor.bean;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Declares that this is a Rimor configuration bean.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RimorConfiguration {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package st.networkers.rimor.command;

import st.networkers.rimor.bean.BeanProcessingException;
import st.networkers.rimor.bean.BeanProcessor;
import st.networkers.rimor.instruction.InstructionResolver;
import st.networkers.rimor.instruction.InstructionResolver.InstructionResolution;
Expand Down Expand Up @@ -30,15 +31,21 @@ public void process(Object bean) {
if (!bean.getClass().isAnnotationPresent(Command.class) || isSubcommand(bean))
return;

MappedCommand command = this.resolve(bean, false);
MappedCommand command = this.resolve(bean);
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, boolean processBean) {
private MappedCommand resolveAndProcessCommandBean(Object bean) {
MappedCommand subcommand = this.resolve(bean);
beanProcessor.process(bean);
return subcommand;
}

public MappedCommand resolve(Object bean) {
if (this.resolvedCommands.containsKey(bean)) {
return this.resolvedCommands.get(bean);
}
Expand All @@ -55,11 +62,6 @@ public MappedCommand resolve(Object bean, boolean processBean) {
.create();

this.resolvedCommands.put(bean, command);

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

return command;
}

Expand All @@ -72,7 +74,7 @@ private List<String> resolveIdentifiers(Object bean) {
.collect(Collectors.toList());

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

return identifiers;
}
Expand All @@ -90,12 +92,11 @@ private Collection<MappedCommand> resolveSubcommands(Object bean) {

private Collection<MappedCommand> resolveRegisteredSubcommands(CommandDefinition bean) {
return bean.getSubcommands().stream()
.map(subcommandBean -> this.resolve(subcommandBean, true))
.map(this::resolveAndProcessCommandBean)
.collect(Collectors.toList());
}

// package-private for testing purposes
Collection<MappedCommand> resolveDeclaredSubcommands(Object bean) {
private Collection<MappedCommand> resolveDeclaredSubcommands(Object bean) {
Collection<Class<?>> classesToIgnore = new ArrayList<>();

if (bean instanceof CommandDefinition) {
Expand All @@ -109,7 +110,7 @@ Collection<MappedCommand> resolveDeclaredSubcommands(Object bean) {
.filter(subcommandClass -> subcommandClass.isAnnotationPresent(Command.class))
.filter(subcommandClass -> !classesToIgnore.contains(subcommandClass))
.map(subcommandClass -> ReflectionUtils.instantiateInnerClass(bean, subcommandClass))
.map(subcommandBean -> this.resolve(subcommandBean, true))
.map(this::resolveAndProcessCommandBean)
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,12 @@
public class ExecutionContext {

public static Builder builder() {
return new Builder(new MatchingMap<>());
}

public static Builder builder(ExecutionContext context) {
return new Builder(new MatchingMap<>(context.components));
return new Builder();
}

// not a simple map because there may be components bound to tokens with required qualifier types,
// and we have to provide them to parameters with instances of them
private final MatchingMap<Token<?>, Object> components;
final MatchingMap<Token<?>, Object> components;

public ExecutionContext(MatchingMap<Token<?>, Object> components) {
this.components = components;
Expand All @@ -47,8 +43,8 @@ public int hashCode() {
public static class Builder {
private final MatchingMap<Token<?>, Object> components;

private Builder(MatchingMap<Token<?>, Object> components) {
this.components = components;
private Builder() {
this.components = new MatchingMap<>();
}

public <T> Builder bind(Class<? super T> type, T object) {
Expand All @@ -61,6 +57,11 @@ public <T> Builder bind(Token<? super T> token, T object) {
return this;
}

public Builder copy(ExecutionContext context) {
this.components.putAll(context.components);
return this;
}

public ExecutionContext build() {
return new ExecutionContext(this.components);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ public interface ExecutionContextService {
*/
<T> Optional<T> get(Token<T> token, Object bean, ExecutionContext context);

/**
* Registers the given {@link ExecutionContextProvider} globally, for all Rimor beans.
*/
void registerGlobalExecutionContextProvider(ExecutionContextProvider<?> executionContextProvider);

/**
* Registers the given {@link ExecutionContextProvider} locally for the given bean.
*/
void registerExecutionContextProvider(ExecutionContextProvider<?> executionContextProvider, Object bean);

/**
* Invokes the given method injecting all its parameters following {@link #get(Token, ExecutionContext)}
*
Expand All @@ -44,4 +54,5 @@ public interface ExecutionContextService {
* @return the result of executing the method
*/
Object invokeMethod(QualifiedMethod qualifiedMethod, Object instance, ExecutionContext context);

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package st.networkers.rimor.context;

import st.networkers.rimor.bean.BeanManager;
import st.networkers.rimor.context.provide.ExecutionContextProvider;
import st.networkers.rimor.context.provide.ExecutionContextProviderRegistry;
import st.networkers.rimor.qualify.reflect.QualifiedMethod;
import st.networkers.rimor.qualify.reflect.QualifiedParameter;
Expand All @@ -13,13 +13,10 @@

public class ExecutionContextServiceImpl implements ExecutionContextService {

private final BeanManager beanManager;

private final ExecutionContextProviderRegistry globalProviderRegistry;
private final Map<Object, ExecutionContextProviderRegistry> beanProviderRegistries = new HashMap<>();

public ExecutionContextServiceImpl(BeanManager beanManager, ExecutionContextProviderRegistry globalProviderRegistry) {
this.beanManager = beanManager;
public ExecutionContextServiceImpl(ExecutionContextProviderRegistry globalProviderRegistry) {
this.globalProviderRegistry = globalProviderRegistry;
}

Expand All @@ -33,9 +30,13 @@ public <T> Optional<T> get(Token<T> token, ExecutionContext context) {

@Override
public <T> Optional<T> get(Token<T> token, Object bean, ExecutionContext context) {
ExecutionContextProviderRegistry beanProviderRegistry = beanProviderRegistries.get(bean);
if (beanProviderRegistry == null)
return this.get(token, context);

return OptionalUtils.firstPresent(
context.get(token),
() -> this.fromProviderRegistry(beanProviderRegistries.get(bean), token, context),
() -> this.fromProviderRegistry(beanProviderRegistry, token, context),
() -> this.fromProviderRegistry(globalProviderRegistry, token, context)
);
}
Expand All @@ -44,6 +45,17 @@ private <T> Optional<T> fromProviderRegistry(ExecutionContextProviderRegistry ex
return executionContextProviderRegistry.findFor(token).map(provider -> provider.get(token, context));
}

@Override
public void registerGlobalExecutionContextProvider(ExecutionContextProvider<?> executionContextProvider) {
this.globalProviderRegistry.register(executionContextProvider);
}

@Override
public void registerExecutionContextProvider(ExecutionContextProvider<?> executionContextProvider, Object bean) {
this.beanProviderRegistries.computeIfAbsent(bean, b -> new ExecutionContextProviderRegistry())
.register(executionContextProvider);
}

@Override
public Object invokeMethod(QualifiedMethod method, Object bean, ExecutionContext context) {
return ReflectionUtils.invoke(method.getMethod(), bean, resolveParameters(method, bean, context));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package st.networkers.rimor.context.provide;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ClassUtils;
import st.networkers.rimor.qualify.DinamicallyQualified;
import st.networkers.rimor.context.ExecutionContext;
import st.networkers.rimor.context.Token;
import st.networkers.rimor.qualify.DinamicallyQualified;
import st.networkers.rimor.qualify.reflect.QualifiedClass;
import st.networkers.rimor.qualify.reflect.QualifiedMethod;

Expand All @@ -17,6 +16,8 @@
/**
* Useful abstract class to implement {@link ExecutionContextProvider}s.
*
* <p>Check {@link ProvidesContext} for annotation-based execution context providers (recommended for most cases).
*
* <p>For example, this is the provider for an imaginary {@code @Stats}-annotated {@code UserStats}, obtained from an
* imaginary {@code @Sender}-annotated {@code User} present in the {@link ExecutionContext execution context}:
* <pre>
Expand Down Expand Up @@ -56,13 +57,14 @@ public abstract class AbstractExecutionContextProvider<T>

@SafeVarargs
protected AbstractExecutionContextProvider(Class<? extends T> providedType, Class<? extends T>... otherTypes) {
this(Stream.concat(Stream.of(providedType), Arrays.stream(ArrayUtils.add(otherTypes, providedType)))
this(Stream.concat(Stream.of(providedType), Arrays.stream(otherTypes))
.map(ClassUtils::primitiveToWrapper)
.collect(Collectors.toList()));
}

protected AbstractExecutionContextProvider(Type providedType, Type... otherTypes) {
this(Stream.concat(Stream.of(providedType), Arrays.stream(otherTypes)).collect(Collectors.toList()));
this(Stream.concat(Stream.of(providedType), Arrays.stream(otherTypes))
.collect(Collectors.toList()));
}

private AbstractExecutionContextProvider(Collection<Type> providedTypes) {
Expand All @@ -83,4 +85,13 @@ private void addPresentAnnotations() {
throw new RuntimeException(e);
}
}

@Override
public String toString() {
return this.getClass().getSimpleName() + "{" +
"providedTypes=" + providedTypes +
", annotations=" + annotations +
", requiredAnnotations=" + requiredAnnotations +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import st.networkers.rimor.util.OptionalUtils;

import java.lang.reflect.Type;
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;

public class ExecutionContextProviderRegistry implements Cloneable {

Expand All @@ -21,7 +23,13 @@ public class ExecutionContextProviderRegistry implements Cloneable {
public void register(ExecutionContextProvider<?> provider) {
for (Type type : provider.getProvidedTypes()) {
Token<?> token = Token.of(type, provider.getQualifiersMap(), provider.getRequiredQualifiers());
this.providers.put(token, provider); // TODO throw if key already present?

this.getFor(token).ifPresent(existingProvider -> {
String message = String.format("found several execution context providers with matching tokens " +
"at the same level: %s, %s", existingProvider, provider);
throw new IllegalStateException(message);
});
this.providers.put(token, provider);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package st.networkers.rimor.context.provide;

import st.networkers.rimor.context.ExecutionContext;
import st.networkers.rimor.context.ExecutionContextService;
import st.networkers.rimor.context.Token;
import st.networkers.rimor.qualify.reflect.QualifiedMethod;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;

public class HandlerMethodExecutionContextProvider implements ExecutionContextProvider<Object> {

private final ExecutionContextService executionContextService;

private final Object bean;
private final QualifiedMethod method;
private final Collection<Type> providedTypes;

public HandlerMethodExecutionContextProvider(ExecutionContextService executionContextService,
Object bean, QualifiedMethod method, Collection<Type> providedTypes) {
this.executionContextService = executionContextService;
this.bean = bean;
this.method = method;
this.providedTypes = providedTypes;
}

@Override
public Object get(Token<Object> token, ExecutionContext context) {
// first, create a context copy with the token and annotations present in this method to make it possible to inject them:
ExecutionContext.Builder contextWithToken = ExecutionContext.builder()
.copy(context)
.bind(new Token<Token<?>>() {}, token);

for (Map.Entry<Class<? extends Annotation>, Annotation> entry : token.getQualifiersMap().entrySet()) {
contextWithToken.bind(Token.of((Type) entry.getKey()), entry.getValue());
}

return executionContextService.invokeMethod(method, bean, contextWithToken.build());
}

public Object getBean() {
return bean;
}

public QualifiedMethod getMethod() {
return method;
}

@Override
public Collection<Type> getProvidedTypes() {
return providedTypes;
}

@Override
public Map<Class<? extends Annotation>, Annotation> getQualifiersMap() {
return method.getQualifiersMap();
}

@Override
public Collection<Class<? extends Annotation>> getRequiredQualifiers() {
return method.getRequiredQualifiers();
}

@Override
public String toString() {
return this.method.getMethod().toString() + " handler method";
}
}
Loading

0 comments on commit 8bca055

Please sign in to comment.