InjectedApp.java

package space.sunqian.common.app.di;

import space.sunqian.annotations.Nonnull;
import space.sunqian.annotations.Nullable;
import space.sunqian.common.Fs;
import space.sunqian.common.app.SimpleApp;
import space.sunqian.common.collect.CollectKit;
import space.sunqian.common.collect.ListKit;
import space.sunqian.common.reflect.TypeRef;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

/**
 * A dependency injection-based sub-interface of {@link SimpleApp}, built via {@link #newBuilder()}. See
 * {@linkplain space.sunqian.common.app.di DI package documentation} for more information.
 *
 * @author sunqian
 */
public interface InjectedApp extends SimpleApp {

    /**
     * Returns a new builder for {@link InjectedApp}.
     *
     * @return a new builder for {@link InjectedApp}
     */
    static @Nonnull Builder newBuilder() {
        return new Builder();
    }

    /**
     * Shuts down this app.
     * <p>
     * All {@code pre-destroy} methods in this app's resources are executed sequentially according to their dependency
     * relationships. If an exception occurs during the execution, an {@link InjectedResourceDestructionException} will
     * be thrown and the shutdown process will terminate immediately, and the remaining {@code pre-destroy} methods will
     * not be executed. See {@linkplain space.sunqian.common.app.di DI package documentation} for more information.
     * <p>
     * Once this app is shut down, it becomes invalid and cannot be restarted. However, its sub-apps are not
     * automatically shut down along with it. Sub-apps that depend on resources from this app will generally continue to
     * function normally, as long as those resources haven't been destroyed (by {@code pre-destroy} methods) during this
     * shutdown process.
     * <p>
     * This method blocks current thread until the shutdown operation is completed.
     *
     * @throws InjectedResourceDestructionException if any error occurs during resource destruction
     * @throws InjectedAppException                 if any other error occurs during shutdown operation
     */
    void shutdown() throws InjectedResourceDestructionException, InjectedAppException;

    /**
     * Returns all parent apps of this app.
     *
     * @return all parent apps of this app
     */
    @Nonnull
    List<@Nonnull InjectedApp> parentApps();

    /**
     * Returns the resource map that are directly owned by this app, excluding the resources inherited from parent
     * apps.
     *
     * @return the resource map that are directly owned by this app, excluding the resources inherited from parent apps
     */
    @Nonnull
    Map<@Nonnull Type, @Nonnull InjectedResource> localResources();

    /**
     * Returns all resource map that constitute this app, including the resources inherited from parent apps.
     *
     * @return all resource map that constitute this app, including the resources inherited from parent apps
     */
    @Nonnull
    Map<@Nonnull Type, @Nonnull InjectedResource> resources();

    /**
     * Returns the resource whose type is assignable to the specified type, or {@code null} if no such resource exists.
     * <p>
     * This method first attempts to find a resource whose type exactly matches the specified type using
     * {@link Object#equals(Object)}. If no exact match is found, it will randomly select one resource that can be
     * assigned to the specified type from all resources that constitute this app.
     *
     * @param type the specified type
     * @return the resource whose type is assignable to the specified type, or {@code null} if no such resource exists
     */
    @Nullable
    InjectedResource getResource(@Nonnull Type type);

    /**
     * Returns the resource object whose type is assignable to the specified type, or {@code null} if no such resource
     * exists.
     * <p>
     * This method first attempts to find a resource whose type exactly matches the specified type using
     * {@link Object#equals(Object)}. If no exact match is found, it will randomly select one resource object that can
     * be assigned to the specified type from all resources that constitute this app.
     *
     * @param <T>  the specified type
     * @param type the specified type
     * @return the resource object whose type is assignable to the specified type, or {@code null} if no such resource
     * exists
     */
    default <T> @Nullable T getObject(@Nonnull Class<T> type) {
        return Fs.as(getObject((Type) type));
    }

    /**
     * Returns the resource object whose type is assignable to the specified type, or {@code null} if no such resource
     * exists.
     * <p>
     * This method first attempts to find a resource whose type exactly matches the specified type using
     * {@link Object#equals(Object)}. If no exact match is found, it will randomly select one resource object that can
     * be assigned to the specified type from all resources that constitute this app.
     *
     * @param <T>  the specified type
     * @param type the {@link TypeRef} for the specified type
     * @return the resource object whose type is assignable to the specified type, or {@code null} if no such resource
     * exists
     */
    default <T> @Nullable T getObject(@Nonnull TypeRef<T> type) {
        return Fs.as(getObject(type.type()));
    }

    /**
     * Returns the resource object whose type is assignable to the specified type, or {@code null} if no such resource
     * exists.
     * <p>
     * This method first attempts to find a resource whose type exactly matches the specified type using
     * {@link Object#equals(Object)}. If no exact match is found, it will randomly select one resource object that can
     * be assigned to the specified type from all resources that constitute this app.
     *
     * @param type the specified type
     * @return the resource object whose type is assignable to the specified type, or {@code null} if no such resource
     * exists
     */
    @Nullable
    Object getObject(@Nonnull Type type);

    /**
     * Builder for {@link InjectedApp}.
     */
    class Builder {

        private static final @Nonnull List<@Nonnull String> RESOURCE_ANNOTATIONS =
            ListKit.list("javax.annotation.Resource", "jakarta.annotation.Resource");
        private static final @Nonnull List<@Nonnull String> POST_CONSTRUCT_ANNOTATIONS =
            ListKit.list("javax.annotation.PostConstruct", "jakarta.annotation.PostConstruct");
        private static final @Nonnull List<@Nonnull String> PRE_DESTROY_ANNOTATIONS =
            ListKit.list("javax.annotation.PreDestroy", "jakarta.annotation.PreDestroy");

        private final @Nonnull Collection<Type> resourceTypes = new LinkedHashSet<>();
        private final @Nonnull Collection<@Nonnull InjectedApp> parentApps = new LinkedHashSet<>();
        private final @Nonnull Collection<@Nonnull String> resourceAnnotations = new LinkedHashSet<>();
        private final @Nonnull Collection<@Nonnull String> postConstructAnnotations = new LinkedHashSet<>();
        private final @Nonnull Collection<@Nonnull String> preDestroyAnnotations = new LinkedHashSet<>();

        private @Nonnull InjectedResource.Resolver resourceResolver = InjectedResource.defaultResolver();
        private @Nonnull InjectedResource.FieldSetter fieldSetter = InjectedResource.defaultFieldSetter();

        /**
         * Adds a resource annotation type that indicates a {@link Field} references a resource from the app.
         * <p>
         * Multiple resource annotation types can be specified, and any of them will be recognized as marking a resource
         * dependency. If no resource annotation types are explicitly specified, the default annotations used are:
         * {@code javax.annotation.Resource} and {@code jakarta.annotation.Resource}.
         *
         * @param resourceAnnotation a resource annotation type that marks a field as referencing an app resource
         * @return this builder
         */
        public @Nonnull Builder resourceAnnotation(@Nonnull Class<? extends Annotation> resourceAnnotation) {
            this.resourceAnnotations.add(resourceAnnotation.getName());
            return this;
        }

        /**
         * Adds a post-construct annotation type that indicates a {@link Method} will be executed after its resource
         * construction is completed.
         * <p>
         * Multiple post-construct annotation types can be specified, and any of them will be recognized as marking a
         * post-construct method. If no post-construct annotation types are explicitly specified, the default
         * annotations used are: {@code javax.annotation.PostConstruct} and {@code jakarta.annotation.PostConstruct}.
         *
         * @param postConstructAnnotation a post-construct annotation type that marks a method to be executed after its
         *                                resource construction is completed
         * @return this builder
         */
        public @Nonnull Builder postConstructAnnotation(@Nonnull Class<? extends Annotation> postConstructAnnotation) {
            this.postConstructAnnotations.add(postConstructAnnotation.getName());
            return this;
        }

        /**
         * Adds a pre-destroy annotation type that indicates a {@link Method} will be executed before its resource is
         * destroyed.
         * <p>
         * Multiple pre-destroy annotation types can be specified, and any of them will be recognized as marking a
         * pre-destroy method. If no pre-destroy annotation types are explicitly specified, the default annotations used
         * are: {@code javax.annotation.PreDestroy} and {@code jakarta.annotation.PreDestroy}.
         *
         * @param preDestroyAnnotation a pre-destroy annotation type that marks a method to be executed before its
         *                             resource is destroyed
         * @return this builder
         */
        public @Nonnull Builder preDestroyAnnotation(@Nonnull Class<? extends Annotation> preDestroyAnnotation) {
            this.preDestroyAnnotations.add(preDestroyAnnotation.getName());
            return this;
        }

        /**
         * Adds root resource types to this builder, each type should be a {@link Class} or {@link ParameterizedType}.
         * And previously added types will be ignored.
         * <p>
         * These types serve as root types for dependency injection. The dependency resolver will recursively analyze
         * {@link Field}s of these types to build the complete dependency graph. Note each type only generates singleton
         * resource instance.
         *
         * @param resourceTypes the resource types to be added
         * @return this builder
         */
        public @Nonnull Builder resourceTypes(@Nonnull Type @Nonnull ... resourceTypes) {
            CollectKit.addAll(this.resourceTypes, resourceTypes);
            return this;
        }

        /**
         * Adds root resource types to this builder, each type should be a {@link Class} or {@link ParameterizedType}.
         * And previously added types will be ignored.
         * <p>
         * These types serve as root types for dependency injection. The dependency resolver will recursively analyze
         * {@link Field}s of these types to build the complete dependency graph. Note each type only generates singleton
         * resource instance.
         *
         * @param resourceTypes the resource types to be added
         * @return this builder
         */
        public @Nonnull Builder resourceTypes(@Nonnull Iterable<@Nonnull Type> resourceTypes) {
            CollectKit.addAll(this.resourceTypes, resourceTypes);
            return this;
        }

        /**
         * Adds parent apps to inherit and share its resources. If a resource type already exists in the parent app, the
         * sub-app will not generate another instance of that type.
         * <p>
         * Once a parent app is shut down, its sub-apps are not automatically shut down along with it. The inherited
         * resources will still be held by the sub-apps, but theirs pre-destroy methods will be executed.
         *
         * @param parentApps the parent apps
         * @return this builder
         */
        public @Nonnull Builder parentApps(@Nullable InjectedApp @Nonnull ... parentApps) {
            CollectKit.addAll(this.parentApps, parentApps);
            return this;
        }

        /**
         * Adds parent apps to inherit and share its resources. If a resource type already exists in the parent app, the
         * sub-app will not generate another instance of that type.
         * <p>
         * Once a parent app is shut down, its sub-apps are not automatically shut down along with it. The inherited
         * resources will still be held by the sub-apps, but theirs pre-destroy methods will be executed.
         *
         * @param parentApps the parent apps
         * @return this builder
         */
        public @Nonnull Builder parentApps(@Nonnull Iterable<@Nonnull InjectedApp> parentApps) {
            CollectKit.addAll(this.parentApps, parentApps);
            return this;
        }

        /**
         * Sets the resource resolver for this app. The default is {@link InjectedResource#defaultResolver()}.
         *
         * @param resourceResolver the resource resolver
         * @return this builder
         */
        public @Nonnull Builder resourceResolver(@Nonnull InjectedResource.Resolver resourceResolver) {
            this.resourceResolver = resourceResolver;
            return this;
        }

        /**
         * Sets the field setter for this app. The default is {@link InjectedResource#defaultFieldSetter()}.
         *
         * @param fieldSetter the field setter
         * @return this builder
         */
        public @Nonnull Builder fieldSetter(@Nonnull InjectedResource.FieldSetter fieldSetter) {
            this.fieldSetter = fieldSetter;
            return this;
        }

        /**
         * Builds and starts a new app instance. The startup process performs the following operations in sequence:
         * <ol>
         *   <li>Generating instances for all resource types with singleton mode;</li>
         *   <li>Dependency injection for all resource instances;</li>
         *   <li>AOP processing if {@link InjectedAspect} instances exist in resource instances;</li>
         *   <li>Re-injection of dependencies for resource instances affected by AOP processing;</li>
         *   <li>Executing all {@code post-construct} methods of resource instances in dependency order</li>
         * </ol>
         * If any {@code post-construct} method fails, an {@link InjectedResourceInitializationException} is thrown
         * immediately. Resources that have already successfully executed {@code post-construct} methods will not be
         * rolled back. See {@linkplain space.sunqian.common.app.di DI package documentation} for more information.
         * <p>
         * This method blocks current thread until the startup operation is completed.
         *
         * @return the newly built and started app instance
         * @throws InjectedResourceInitializationException if any {@code post-construct} method execution fails
         * @throws InjectedAppException                    if any other error occurs during app construction or startup
         */
        public @Nonnull InjectedApp build()
            throws InjectedResourceInitializationException, InjectedAppException {
            if (resourceAnnotations.isEmpty()) {
                resourceAnnotations.addAll(RESOURCE_ANNOTATIONS);
            }
            if (postConstructAnnotations.isEmpty()) {
                postConstructAnnotations.addAll(POST_CONSTRUCT_ANNOTATIONS);
            }
            if (preDestroyAnnotations.isEmpty()) {
                preDestroyAnnotations.addAll(PRE_DESTROY_ANNOTATIONS);
            }
            return new InjectedAppImpl(
                resourceTypes,
                parentApps,
                resourceAnnotations,
                postConstructAnnotations,
                preDestroyAnnotations,
                resourceResolver,
                fieldSetter
            );
        }
    }
}