BeanProvider.java

/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2011-2026 Hazendaz
 */
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 com.github.hazendaz.beanprovider;

import com.github.hazendaz.beanprovider.internal.deltaspike.metadata.builder.AnnotatedTypeBuilder;

import jakarta.enterprise.context.spi.CreationalContext;
import jakarta.enterprise.inject.Vetoed;
import jakarta.enterprise.inject.spi.AnnotatedType;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.inject.spi.InjectionTarget;
import jakarta.inject.Inject;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.deltaspike.core.util.metadata.AnnotationInstanceProvider;

/**
 * Customized BeanProvider which allows to perform field-injection on non-CDI managed classes.
 * <p>
 * This class contains utility methods to resolve contextual references in situations where no injection is available
 * because the current class is not managed by the CDI Container. This can happen in e.g. a JPA-2.0 EntityListener, a
 * ServletFilter, a Spring managed Bean, etc.
 * <p>
 * <b>Attention:</b> This method is intended for being used in user code at runtime. If this method gets used during
 * Container boot (in an Extension), non-portable behavior results. The CDI specification only allows injection of the
 * BeanManager during CDI-Container boot time.
 */
@Vetoed
public final class BeanProvider {

    /**
     * Allows to perform dependency injection for instances which aren't managed by CDI.
     * <p>
     * Attention: The resulting instance isn't managed by CDI; only fields annotated with {@link Inject} (or marked with
     * {@link PostInject} and then synthesized to {@link Inject}) get initialized.
     *
     * @param instance
     *            current instance (required)
     * @param <T>
     *            current type
     * @param ignoreMap
     *            map of field names to annotation types which should be removed from those fields (required)
     *
     * @return the same instance with injected fields
     */
    public static <T> T injectFields(final T instance, final Map<String, Class<? extends Annotation>> ignoreMap) {
        if (instance == null) {
            throw new IllegalArgumentException("BeanProvider `injectFields` method requires a non-null instance.");
        }
        if (ignoreMap == null) {
            throw new IllegalArgumentException("BeanProvider `injectFields` method requires a non-null ignoreMap.");
        }

        // Initialize processing using core 'deltaspike' bean provider
        final BeanManager beanManager = BeanProvider.getBeanManager();

        // Handle 'ignoreMap' customization to injection
        @SuppressWarnings("unchecked")
        final AnnotatedTypeBuilder<Object> builder = new AnnotatedTypeBuilder<>()
                .readFromType((AnnotatedType<Object>) beanManager.createAnnotatedType(instance.getClass()), true);

        // Remove annotations as specified in ignoreMap
        for (final Entry<String, Class<? extends Annotation>> entry : ignoreMap.entrySet()) {
            final String fieldName = entry.getKey();
            if (fieldName == null) {
                throw new IllegalArgumentException("BeanProvider `ignoreMap` contains a null field name.");
            }

            final Class<? extends Annotation> annotationType = entry.getValue();
            if (annotationType == null) {
                throw new IllegalArgumentException(
                        "BeanProvider `ignoreMap` contains a null annotation type for field: " + fieldName);
            }

            final Field field;
            try {
                field = instance.getClass().getDeclaredField(fieldName);
            } catch (final NoSuchFieldException e) {
                throw new IllegalArgumentException("BeanProvider `ignoreMap` references missing field: " + fieldName,
                        e);
            }

            builder.removeFromField(field, annotationType);
        }

        // Support @PostInject annotation by adding @Inject along side it
        final Annotation injectAnnotation = AnnotationInstanceProvider.of(Inject.class);
        for (final Field field : instance.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(PostInject.class)) {
                builder.addToField(field, injectAnnotation);
            }
        }

        // Finalize processing using core 'deltaspike' bean provider
        final CreationalContext<Object> creationalContext = beanManager.createCreationalContext(null);

        final AnnotatedType<Object> annotatedType = builder.create();
        final InjectionTarget<Object> injectionTarget = beanManager.getInjectionTargetFactory(annotatedType)
                .createInjectionTarget(null);
        injectionTarget.inject(instance, creationalContext);
        return instance;
    }

    /**
     * Internal method to resolve the BeanManager.
     *
     * @return current bean-manager
     */
    private static BeanManager getBeanManager() {
        return CDI.current().getBeanManager();
    }

    private BeanProvider() {
        // this is a utility class which doesn't get instantiated.
    }

}