Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CDI integration #105

Open
rmannibucau opened this issue Apr 5, 2022 · 0 comments
Open

CDI integration #105

rmannibucau opened this issue Apr 5, 2022 · 0 comments

Comments

@rmannibucau
Copy link
Contributor

Hi,

Makes a few projects I'm integrating crest with CDI and goals of this issue are: 1. at least share how to do it, 2. show some limitation of crest currently (the biggest being the env threadlocal but I'm not sure how we would like to resolve it without breaking).

Integration

Personally I extend crest with 4 "points":

  • a CDI aware main to scan with CDI the commands
  • a CDI extension to make @Command a qualifier and optionally adds it on classes using @Command at method only level. All annotation methods will be set as @NonBinding
  • a CDI aware environment for service injection (command parameters)
  • a CDI target indeed to use the right bean instance

CDI Main

public class CdiCrestMain extends Main {
    private final BeanManager beanManager;
    private final DefaultsContext context;

    public CdiCrestMain(final BeanManager beanManager, final DefaultsContext context) {
        super(context);
        this.beanManager = beanManager;
        this.context = context;
        registerCommands();
    }

    public Map<String, Cmd> commands() {
        return commands;
    }

    private void registerCommands() {
        final var qualifier = new CommandQualifier();
        this.beanManager.getBeans(Object.class, qualifier)
                .forEach(bean -> commands.putAll(Commands.get(bean.getBeanClass(), new CdiTarget(beanManager, bean), context)));
    }

    public void apply(final String... args) throws Exception {
        final var environment = new CDIEnvironment(beanManager);
        final var envHolder = Environment.ENVIRONMENT_THREAD_LOCAL;
        envHolder.set(environment);
        try {
            super.main(environment, args);
        } finally {
            // crest API has a default value so we can't really store previous value and reset it
            envHolder.remove();
        }
    }

    private static class CommandQualifier extends AnnotationLiteral<Command> implements Command {
        @Override
        public String value() {
            return null;
        }

        @Override
        public String usage() {
            return null;
        }

        @Override
        public Class<?>[] interceptedBy() {
            return new Class[0];
        }
    }
}

Here see the limitatio nof the threadlocal in apply, we should be able to stack it to reset it after usage but here we can't since it has a default. An option is Environment.hasValue() which would avoid the default and Environment.value() but it would break backward compatibility so not sure the goal. I would also prefer the environment to be passed to the commands like the context and not depend on a thread local.

CDI Environment

public class CDIEnvironment extends SystemEnvironment {
    private final BeanManager beanManager;

    public CDIEnvironment(final BeanManager beanManager) {
        this.beanManager = beanManager;
    }

    @Override
    public <T> T findService(final Class<T> type) {
        return ofNullable(super.findService(type))
                .orElseGet(() -> {
                    final var bean = beanManager.resolve(beanManager.getBeans(type));
                    if (bean == null || !beanManager.isNormalScope(bean.getScope())) {
                        throw new IllegalStateException("For now only normal scoped beans can be used as command parameter injection.");
                    }
                    return type.cast(beanManager.getReference(bean, type, beanManager.createCreationalContext(null)));
                });
    }
}

We can make it supporting not normal scoped beans - not sure it is that useful, maybe, but it means storing the instances for the call duration and release the context after. here the API should be enhanced to return a CrestInstance() { T value(); void close(); } which would be automatically stored/unwrapped by the runtime.

CDI Target

public class CdiTarget implements Target {
    private final BeanManager beanManager;
    private final Bean<?> bean;

    public CdiTarget(final BeanManager beanManager, final Bean<?> bean) {
        this.beanManager = beanManager;
        this.bean = bean;
    }

    @Override
    public Object invoke(final Method method, final Object... args) throws InvocationTargetException, IllegalAccessException {
        return method.invoke(getInstance(method), args);
    }

    @Override
    public Object getInstance(final Method method) {
        final var creationalContext = beanManager.createCreationalContext(null);
        return beanManager.getReference(bean, bean.getBeanClass(), creationalContext);
    }
}

Here again we have the same limitation than for the services, CrestInstance can solve it. Theorically we should be able to have a more adapted API since getInstance is used for bean validation whereas we should get something like <T, Object> T withInstance(Function<T, Object>) API which would wrap the bean validation+method invocation to lookup a single time the instance and be able to release it after method invocation (which would need the instance to call injected in parameters for ex).
Here again, not sure in terms of backward compatibility what is desired.

CDI Extension to make @Command a qualifier

This one is not 100% required and can be replaced by an extension for the scanning but this implementation is faster (always better for a CLI) even if it has the limitation to not enable to have alternatives for commands - which is not a big limitation since it can be replaced by a small indirection if needed (never?).

public class CrestCommandQualifierExtension implements Extension {
    public void onStart(@Observes final BeforeBeanDiscovery beforeBeanDiscovery,
                        final BeanManager beanManager) {
        beforeBeanDiscovery.addQualifier(new NonBindingType<>(beanManager.createAnnotatedType(Command.class)));
    }

    private static class NonBindingType<T> implements AnnotatedType<T> {
        private final AnnotatedType<T> type;
        private final Set<AnnotatedMethod<? super T>> methods;

        private NonBindingType(final AnnotatedType<T> annotatedType) {
            this.type = annotatedType;
            this.methods = annotatedType.getMethods().stream()
                    .map(NonBindingMethod::new)
                    .collect(toSet());
        }

        @Override
        public Class<T> getJavaClass() {
            return type.getJavaClass();
        }

        @Override
        public Set<AnnotatedConstructor<T>> getConstructors() {
            return type.getConstructors();
        }

        @Override
        public Set<AnnotatedMethod<? super T>> getMethods() {
            return methods;
        }

        @Override
        public Set<AnnotatedField<? super T>> getFields() {
            return type.getFields();
        }

        @Override
        public <T1 extends Annotation> Set<T1> getAnnotations(final Class<T1> annotationType) {
            return type.getAnnotations(annotationType);
        }

        @Override
        public Type getBaseType() {
            return type.getBaseType();
        }

        @Override
        public Set<Type> getTypeClosure() {
            return type.getTypeClosure();
        }

        @Override
        public <X extends Annotation> X getAnnotation(final Class<X> aClass) {
            return type.getAnnotation(aClass);
        }

        @Override
        public Set<Annotation> getAnnotations() {
            return type.getAnnotations();
        }

        @Override
        public boolean isAnnotationPresent(final Class<? extends Annotation> aClass) {
            return type.isAnnotationPresent(aClass);
        }
    }

    private static class NonBindingMethod<A> implements AnnotatedMethod<A> {
        private final AnnotatedMethod<A> delegate;
        private final Set<Annotation> annotations;

        private NonBindingMethod(final AnnotatedMethod<A> delegate) {
            this.delegate = delegate;
            this.annotations = Stream.concat(
                            delegate.getAnnotations().stream(),
                            Stream.of(Nonbinding.Literal.INSTANCE))
                    .collect(toSet());
        }

        @Override
        public Method getJavaMember() {
            return delegate.getJavaMember();
        }

        @Override
        public <T extends Annotation> Set<T> getAnnotations(final Class<T> annotationType) {
            return delegate.getAnnotations(annotationType);
        }

        @Override
        public List<AnnotatedParameter<A>> getParameters() {
            return delegate.getParameters();
        }

        @Override
        public boolean isStatic() {
            return delegate.isStatic();
        }

        @Override
        public AnnotatedType<A> getDeclaringType() {
            return delegate.getDeclaringType();
        }

        @Override
        public Type getBaseType() {
            return delegate.getBaseType();
        }

        @Override
        public Set<Type> getTypeClosure() {
            return delegate.getTypeClosure();
        }

        @Override
        public <T extends Annotation> T getAnnotation(final Class<T> aClass) {
            return aClass == Nonbinding.class ? aClass.cast(Nonbinding.Literal.INSTANCE) : delegate.getAnnotation(aClass);
        }

        @Override
        public Set<Annotation> getAnnotations() {
            return annotations;
        }

        @Override
        public boolean isAnnotationPresent(final Class<? extends Annotation> aClass) {
            return Nonbinding.class == aClass || delegate.isAnnotationPresent(aClass);
        }
    }
}

To support method only level commands you can also add:

    public <T> void markAtClassLevelMethodOnlyCommands(@Observes final ProcessAnnotatedType<T> pat) {
        final var annotatedType = pat.getAnnotatedType();
        if (!annotatedType.isAnnotationPresent(Command.class) &&
                annotatedType.getMethods().stream().anyMatch(m -> m.isAnnotationPresent(Command.class))) {
            pat.configureAnnotatedType().add(new AnnotationLiteral<Command>() { // todo: extract CommandQualifier from main to make it a constant if this impl is desired 
            });
        }
    }

I tend to avoid to use this since it makes the extenson o(n) instead of o(1).
I also use the extension without the SPI registration using CDI SE API:

public static void main(final String... args) throws Exception {
        try (final var container = SeContainerInitializer.newInstance()
                .addExtensions(new CrestCommandQualifierExtension())
                .initialize()) {
            new CdiCrestMain(container.getBeanManager(), new SystemPropertiesDefaultsContext()).apply(args);
        }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant