All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.data.runtime.intercept.AbstractQueryInterceptor Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017-2020 original authors
 *
 * Licensed 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
 *
 * https://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 io.micronaut.data.runtime.intercept;

import io.micronaut.aop.InvocationContext;
import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.beans.BeanWrapper;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.MutableArgumentValue;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.annotation.TypeRole;
import io.micronaut.data.exceptions.EmptyResultException;
import io.micronaut.data.intercept.DataInterceptor;
import io.micronaut.data.intercept.RepositoryMethodKey;
import io.micronaut.data.intercept.annotation.DataMethod;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.PersistentEntity;
import io.micronaut.data.model.PersistentProperty;
import io.micronaut.data.model.Sort;
import io.micronaut.data.model.runtime.AbstractPreparedDataOperation;
import io.micronaut.data.model.runtime.BatchOperation;
import io.micronaut.data.model.runtime.DefaultStoredDataOperation;
import io.micronaut.data.model.runtime.DeleteBatchOperation;
import io.micronaut.data.model.runtime.DeleteOperation;
import io.micronaut.data.model.runtime.DeleteReturningBatchOperation;
import io.micronaut.data.model.runtime.DeleteReturningOperation;
import io.micronaut.data.model.runtime.EntityInstanceOperation;
import io.micronaut.data.model.runtime.EntityOperation;
import io.micronaut.data.model.runtime.InsertBatchOperation;
import io.micronaut.data.model.runtime.InsertOperation;
import io.micronaut.data.model.runtime.PagedQuery;
import io.micronaut.data.model.runtime.PreparedQuery;
import io.micronaut.data.model.runtime.StoredQuery;
import io.micronaut.data.model.runtime.UpdateBatchOperation;
import io.micronaut.data.model.runtime.UpdateOperation;
import io.micronaut.data.operations.HintsCapableRepository;
import io.micronaut.data.operations.RepositoryOperations;
import io.micronaut.data.runtime.query.DefaultPagedQueryResolver;
import io.micronaut.data.runtime.query.DefaultPreparedQueryResolver;
import io.micronaut.data.runtime.query.DefaultStoredQueryResolver;
import io.micronaut.data.runtime.query.MethodContextAwareStoredQueryDecorator;
import io.micronaut.data.runtime.query.PagedQueryResolver;
import io.micronaut.data.runtime.query.PreparedQueryDecorator;
import io.micronaut.data.runtime.query.PreparedQueryResolver;
import io.micronaut.data.runtime.query.StoredQueryDecorator;
import io.micronaut.data.runtime.query.StoredQueryResolver;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static io.micronaut.data.intercept.annotation.DataMethod.META_MEMBER_LIMIT;
import static io.micronaut.data.intercept.annotation.DataMethod.META_MEMBER_OFFSET;
import static io.micronaut.data.intercept.annotation.DataMethod.META_MEMBER_PAGE_SIZE;

/**
 * Abstract interceptor that executes a {@link Query}.
 *
 * @param  The declaring type
 * @param  The return type
 * @author graemerocher
 * @since 1.0
 */
public abstract class AbstractQueryInterceptor implements DataInterceptor {
    protected final ConversionService conversionService;
    protected final RepositoryOperations operations;
    protected final PreparedQueryResolver preparedQueryResolver;
    private final ConcurrentMap countQueries = new ConcurrentHashMap<>(50);
    private final ConcurrentMap queries = new ConcurrentHashMap<>(50);
    private final StoredQueryResolver storedQueryResolver;
    private final MethodContextAwareStoredQueryDecorator storedQueryDecorator;
    private final PagedQueryResolver pagedQueryResolver;
    private final PreparedQueryDecorator preparedQueryDecorator;

    /**
     * Default constructor.
     *
     * @param operations The operations
     */
    protected AbstractQueryInterceptor(@NonNull RepositoryOperations operations) {
        ArgumentUtils.requireNonNull("operations", operations);
        this.conversionService = operations.getConversionService();
        this.operations = operations;
        this.storedQueryResolver = operations instanceof StoredQueryResolver sQueryResolver ? sQueryResolver : new DefaultStoredQueryResolver() {
            @Override
            protected HintsCapableRepository getHintsCapableRepository() {
                return operations;
            }
        };
        if (operations instanceof MethodContextAwareStoredQueryDecorator methodDecorator) {
            storedQueryDecorator = methodDecorator;
        } else if (operations instanceof StoredQueryDecorator decorator) {
            storedQueryDecorator = new MethodContextAwareStoredQueryDecorator() {
                @Override
                public  StoredQuery decorate(MethodInvocationContext context, StoredQuery storedQuery) {
                    return decorator.decorate(storedQuery);
                }
            };
        } else {
            storedQueryDecorator = new MethodContextAwareStoredQueryDecorator() {
                @Override
                public  StoredQuery decorate(MethodInvocationContext context, StoredQuery storedQuery) {
                    return storedQuery;
                }
            };
        }
        this.preparedQueryResolver = operations instanceof PreparedQueryResolver resolver ? resolver : new DefaultPreparedQueryResolver() {
            @Override
            protected ConversionService getConversionService() {
                return operations.getConversionService();
            }
        };
        this.preparedQueryDecorator = operations instanceof PreparedQueryDecorator decorator ? decorator : new PreparedQueryDecorator() {
            @Override
            public  PreparedQuery decorate(PreparedQuery preparedQuery) {
                return preparedQuery;
            }
        };
        this.pagedQueryResolver = operations instanceof PagedQueryResolver resolver ? resolver : new DefaultPagedQueryResolver();
    }

    /**
     * Returns parameter values with respect of {@link Parameter} annotation.
     *
     * @param context The method invocation context
     * @return The parameters value map
     */
    @NonNull
    protected Map getParameterValueMap(MethodInvocationContext context) {
        Argument[] arguments = context.getArguments();
        Object[] parameterValues = context.getParameterValues();
        Map valueMap = new LinkedHashMap<>(arguments.length);
        for (int i = 0; i < parameterValues.length; i++) {
            Object parameterValue = parameterValues[i];
            Argument arg = arguments[i];
            valueMap.put(arg.getAnnotationMetadata().stringValue(Parameter.class).orElseGet(arg::getName), parameterValue);
        }
        return valueMap;
    }

    /**
     * Returns the return type.
     *
     * @param context The context
     * @return the return type
     */
    protected Argument getReturnType(MethodInvocationContext context) {
        return context.getReturnType().asArgument();
    }

    @Nullable
    protected final Object convertOne(MethodInvocationContext context, @Nullable Object o) {
        Argument argumentType = getReturnType(context);
        Class type = argumentType.getType();
        if (o == null) {
            if (type == Optional.class) {
                return Optional.empty();
            }
            if (argumentType.isDeclaredNonNull() || !argumentType.isNullable()
                    && !context.getReturnType().asArgument().isNullable()) {
                throw new EmptyResultException();
            }
            return null;
        }
        boolean isOptional = false;
        if (type == Optional.class) {
            argumentType = argumentType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
            isOptional = true;
        }
        o = convertOne(o, argumentType);
        if (isOptional) {
            return Optional.of(o);
        }
        return o;
    }

    protected final Object convertOne(Object o, Argument argumentType) {
        if (argumentType.isInstance(o)) {
            return o;
        }
        return operations.getConversionService().convertRequired(o, argumentType);
    }

    /**
     * Prepares a query for the given context.
     *
     * @param        The result generic type
     * @param methodKey  The method key
     * @param context    The context
     * @return The query
     */
    @NonNull
    protected final  PreparedQuery prepareQuery(RepositoryMethodKey methodKey,
                                                           MethodInvocationContext context) {
        validateNullArguments(context);
        StoredQuery storedQuery = findStoreQuery(methodKey, context);
        Pageable pageable = storedQuery.hasPageable() ? getPageable(context) : Pageable.UNPAGED;
        PreparedQuery preparedQuery = preparedQueryResolver.resolveQuery(context, storedQuery, pageable);
        return preparedQueryDecorator.decorate(preparedQuery);
    }

    private  StoredQuery findStoreQuery(MethodInvocationContext context) {
        RepositoryMethodKey key = new RepositoryMethodKey(context.getTarget(), context.getExecutableMethod());
        return findStoreQuery(key, context);
    }

    private  StoredQuery findStoreQuery(RepositoryMethodKey methodKey, MethodInvocationContext context) {
        StoredQuery storedQuery = queries.get(methodKey);
        if (storedQuery == null) {
            storedQuery = storedQueryResolver.resolveQuery(context);
            storedQuery = storedQueryDecorator.decorate(context, storedQuery);
            queries.put(methodKey, storedQuery);
        }
        return storedQuery;
    }

    /**
     * Prepares a query for the given context.
     *
     * @param methodKey The method key
     * @param context   The context
     * @return The query
     */
    @NonNull
    protected final PreparedQuery prepareCountQuery(RepositoryMethodKey methodKey, @NonNull MethodInvocationContext context) {
        StoredQuery storedQuery = countQueries.get(methodKey);
        if (storedQuery == null) {
            storedQuery = storedQueryResolver.resolveCountQuery(context);
            storedQuery = storedQueryDecorator.decorate(context, storedQuery);
            countQueries.put(methodKey, storedQuery);
        }

        Pageable pageable = storedQuery.hasPageable() ? getPageable(context) : Pageable.UNPAGED;
        //noinspection unchecked
        PreparedQuery preparedQuery = preparedQueryResolver.resolveCountQuery(context, storedQuery, pageable);
        return preparedQueryDecorator.decorate(preparedQuery);
    }

    /**
     * Obtains the root entity or throws an exception if it is not available.
     *
     * @param context The context
     * @param      The entity type
     * @return The root entity type
     * @throws IllegalStateException If the root entity is unavailable
     */
    @NonNull
    protected  Class getRequiredRootEntity(MethodInvocationContext context) {
        Class aClass = context.classValue(DataMethod.NAME, DataMethod.META_MEMBER_ROOT_ENTITY).orElse(null);
        if (aClass != null) {
            return aClass;
        } else {
            final AnnotationValue ann = context.getDeclaredAnnotation(DataMethod.NAME);
            if (ann != null) {
                aClass = ann.classValue(DataMethod.META_MEMBER_ROOT_ENTITY).orElse(null);
                if (aClass != null) {
                    return aClass;
                }
            }

            throw new IllegalStateException("No root entity present in method");
        }
    }

    /**
     * Retrieve an entity parameter value in role.
     *
     * @param context The context
     * @param type    The type
     * @param     The generic type
     * @return An result
     */
    protected  RT getEntityParameter(MethodInvocationContext context, @NonNull Class type) {
        return getRequiredParameterInRole(context, TypeRole.ENTITY, type);
    }

    /**
     * Retrieve an entities parameter value in role.
     *
     * @param context The context
     * @param type    The type
     * @param     The generic type
     * @return An result
     */
    protected  Iterable getEntitiesParameter(MethodInvocationContext context, @NonNull Class type) {
        return getRequiredParameterInRole(context, TypeRole.ENTITIES, Iterable.class);
    }

    /**
     * Find an entity parameter value in role.
     *
     * @param context The context
     * @param type    The type
     * @param     The generic type
     * @return An result
     */
    protected  Optional findEntityParameter(MethodInvocationContext context, @NonNull Class type) {
        return getParameterInRole(context, TypeRole.ENTITY, type);
    }

    /**
     * Fid an entities parameter value in role.
     *
     * @param context The context
     * @param type    The type
     * @param     The generic type
     * @return An result
     */
    protected  Optional> findEntitiesParameter(MethodInvocationContext context, @NonNull Class type) {
        Optional parameterInRole = getParameterInRole(context, TypeRole.ENTITIES, Iterable.class);
        return (Optional>) parameterInRole;
    }

    /**
     * Retrieve a parameter in the given role for the given type.
     *
     * @param context The context
     * @param role    The role
     * @param type    The type
     * @param     The generic type
     * @return An result
     */
    protected  RT getRequiredParameterInRole(MethodInvocationContext context, @NonNull String role, @NonNull Class type) {
        return getParameterInRole(context, role, type).orElseThrow(() -> new IllegalStateException("Cannot find parameter with role: " + role));
    }

    /**
     * Retrieve a parameter in the given role for the given type.
     *
     * @param context The context
     * @param role    The role
     * @param type    The type
     * @param     The generic type
     * @return An optional result
     */
    protected  Optional getParameterInRole(MethodInvocationContext context, @NonNull String role, @NonNull Class type) {
        return context.stringValue(DataMethod.NAME, role).flatMap(name -> {
            RT parameterValue = null;
            Map> params = context.getParameters();
            MutableArgumentValue arg = params.get(name);
            if (arg != null) {
                Object o = arg.getValue();
                if (o != null) {
                    if (type.isInstance(o)) {
                        //noinspection unchecked
                        parameterValue = (RT) o;
                    } else {
                        parameterValue = operations.getConversionService()
                                .convert(o, type).orElse(null);
                    }
                }
            }
            return Optional.ofNullable(parameterValue);
        });
    }

    /**
     * Resolves the {@link Pageable} for the given context.
     *
     * @param context The pageable
     * @return The pageable or null
     */
    @NonNull
    protected Pageable getPageable(MethodInvocationContext context) {
        Pageable pageable = getPageableInRole(context);
        if (pageable == null) {
            pageable = Pageable.UNPAGED;
            int limit = context.intValue(DataMethod.NAME, META_MEMBER_PAGE_SIZE)
                .orElseGet(() -> context.intValue(DataMethod.NAME, META_MEMBER_LIMIT).orElse(-1));
            if (limit > 0) {
                pageable = Pageable.from(0, limit);
            }
            Sort sort = getParameterInRole(context, TypeRole.SORT, Sort.class).orElse(null);
            if (sort != null) {
                return pageable.orders(sort.getOrderBy());
            }
        }
        return pageable;
    }

    /**
     * Resolves the {@link Pageable} for the given context.
     *
     * @param context The context
     * @return The pageable or null
     */
    @Nullable
    protected Pageable getPageableInRole(MethodInvocationContext context) {
        return getParameterInRole(context, TypeRole.PAGEABLE, Pageable.class).orElse(null);
    }

    /**
     * Resolves the offset.
     *
     * @param context The context
     * @return The offset or -1
     * @since 4.10
     */
    protected int getOffset(MethodInvocationContext context) {
        return context.intValue(DataMethod.class, META_MEMBER_OFFSET).orElse(-1);
    }

    /**
     * Resolves the limit.
     *
     * @param context The context
     * @return The limit or -1
     * @since 4.10
     */
    protected int getLimit(MethodInvocationContext context) {
        return context.intValue(DataMethod.class, META_MEMBER_LIMIT).orElse(-1);
    }

    /**
     * Return whether the metadata indicates the instance is nullable.
     *
     * @param metadata The metadata
     * @return True if it is nullable
     * @deprecated Not used
     */
    @Deprecated(forRemoval = true, since = "4.10")
    protected boolean isNullable(@NonNull AnnotationMetadata metadata) {
        return metadata
                .getDeclaredAnnotationNames()
                .stream()
                .anyMatch(n -> NameUtils.getSimpleName(n).equalsIgnoreCase("nullable"));
    }

    /**
     * Looks up the entity to persist from the execution context, or throws an exception.
     *
     * @param context The context
     * @return The entity
     */
    @NonNull
    protected Object getRequiredEntity(MethodInvocationContext context) {
        String entityParam = context.stringValue(DataMethod.NAME, TypeRole.ENTITY)
                .orElseThrow(() -> new IllegalStateException("No entity parameter specified"));

        Object o = context.getParameterValueMap().get(entityParam);
        if (o == null) {
            throw new IllegalArgumentException("Entity argument cannot be null");
        }
        return o;
    }

    /**
     * Instantiate the given entity for the given parameter values.
     *
     * @param rootEntity      The entity
     * @param parameterValues The parameter values
     * @return The entity
     * @throws IllegalArgumentException if the entity cannot be instantiated due to an illegal argument
     */
    @NonNull
    protected Object instantiateEntity(@NonNull Class rootEntity, @NonNull Map parameterValues) {
        PersistentEntity entity = operations.getEntity(rootEntity);
        BeanIntrospection introspection = BeanIntrospection.getIntrospection(rootEntity);
        Argument[] constructorArguments = introspection.getConstructorArguments();
        Object instance;
        if (ArrayUtils.isNotEmpty(constructorArguments)) {

            Object[] arguments = new Object[constructorArguments.length];
            boolean strictNullable = true;
            for (int i = 0; i < constructorArguments.length; i++) {
                Argument argument = constructorArguments[i];

                String argumentName = argument.getName();
                Object v = parameterValues.get(argumentName);
                AnnotationMetadata argMetadata = argument.getAnnotationMetadata();
                if (v == null && !PersistentProperty.isNullableMetadata(argMetadata)) {
                    PersistentProperty prop = entity.getPropertyByName(argumentName);
                    if (prop == null || prop.isRequired()) {
                        throw new IllegalArgumentException("Argument [" + argumentName + "] cannot be null");
                    } else {
                        // If Optional or AutoGenerated or AutoPopulated with null value
                        // is one of fields, then we shouldn't force strict nullability check
                        strictNullable = false;
                    }
                }
                arguments[i] = v;
            }
            instance = introspection.instantiate(strictNullable, arguments);
        } else {
            instance = introspection.instantiate();
        }

        BeanWrapper wrapper = BeanWrapper.getWrapper(instance);
        Collection persistentProperties = entity.getPersistentProperties();
        PersistentProperty identity = entity.getIdentity();
        if (identity != null) {
            setProperty(wrapper, identity, parameterValues);
        } else {
            PersistentProperty[] compositeIdentities = entity.getCompositeIdentity();
            if (compositeIdentities != null && compositeIdentities.length > 0) {
                for (PersistentProperty compositeIdentity : compositeIdentities) {
                    setProperty(wrapper, compositeIdentity, parameterValues);
                }
            }
        }
        for (PersistentProperty prop : persistentProperties) {
            setProperty(wrapper, prop, parameterValues);
        }
        return instance;
    }

    /**
     * Get the paged query for the given context.
     *
     * @param context The context
     * @param      The entity type
     * @return The paged query
     */
    @NonNull
    protected  PagedQuery getPagedQuery(@NonNull MethodInvocationContext context) {
        return pagedQueryResolver.resolveQuery(context, getRequiredRootEntity(context), getPageable(context));
    }

    /**
     * Get the insert batch operation for the given context.
     *
     * @param context  The context
     * @param iterable The iterable
     * @param       The entity type
     * @return The paged query
     */
    @NonNull
    protected  InsertBatchOperation getInsertBatchOperation(@NonNull MethodInvocationContext context, @NonNull Iterable iterable) {
        @SuppressWarnings("unchecked") Class rootEntity = getRequiredRootEntity(context);
        return getInsertBatchOperation(context, rootEntity, iterable);
    }

    /**
     * Get the insert batch operation for the given context.
     *
     * @param         The entity type
     * @param context    The context
     * @param rootEntity The root entity
     * @param iterable   The iterable
     * @return The paged query
     */
    @NonNull
    protected  InsertBatchOperation getInsertBatchOperation(@NonNull MethodInvocationContext context, Class rootEntity, @NonNull Iterable iterable) {
        return new DefaultInsertBatchOperation<>(context, rootEntity, iterable);
    }

    /**
     * Get the batch operation for the given context.
     *
     * @param context The context
     * @param      The entity type
     * @return The paged query
     */
    @SuppressWarnings("unchecked")
    @NonNull
    protected  InsertOperation getInsertOperation(@NonNull MethodInvocationContext context) {
        E o = (E) getRequiredEntity(context);
        return new DefaultInsertOperation<>(context, o);
    }

    /**
     * Get the batch operation for the given context.
     *
     * @param context The context
     * @param      The entity type
     * @return The paged query
     */
    @SuppressWarnings("unchecked")
    @NonNull
    protected  UpdateOperation getUpdateOperation(@NonNull MethodInvocationContext context) {
        return getUpdateOperation(context, (E) getRequiredEntity(context));
    }

    /**
     * Get the batch operation for the given context.
     *
     * @param context The context
     * @param entity  The entity instance
     * @param      The entity type
     * @return The paged query
     */
    @SuppressWarnings("unchecked")
    @NonNull
    protected  UpdateOperation getUpdateOperation(@NonNull MethodInvocationContext context, E entity) {
        return new DefaultUpdateOperation<>(context, entity);
    }

    /**
     * Get the update all batch operation for the given context.
     *
     * @param         The entity type
     * @param rootEntity The root entitry
     * @param context    The context
     * @param iterable   The iterable
     * @return The paged query
     */
    @NonNull
    protected  UpdateBatchOperation getUpdateAllBatchOperation(@NonNull MethodInvocationContext context, Class rootEntity, @NonNull Iterable iterable) {
        return new DefaultUpdateBatchOperation<>(context, rootEntity, iterable);
    }

    /**
     * Get the delete operation for the given context.
     *
     * @param context The context
     * @param entity  The entity
     * @param      The entity type
     * @return The paged query
     */
    @NonNull
    protected  DeleteOperation getDeleteOperation(@NonNull MethodInvocationContext context, @NonNull E entity) {
        return new DefaultDeleteOperation<>(context, entity);
    }

    /**
     * Get the delete operation for the given context.
     *
     * @param context The context
     * @param entity  The entity
     * @param      The entity type
     * @param      The result type
     * @return The paged query
     */
    @NonNull
    @Experimental
    protected  DeleteReturningOperation getDeleteReturningOperation(@NonNull MethodInvocationContext context, @NonNull E entity) {
        return new DefaultDeleteReturningOperation<>(context, entity);
    }

    /**
     * Get the delete all batch operation for the given context.
     *
     * @param context The context
     * @param      The entity type
     * @return The paged query
     */
    @NonNull
    protected  DeleteBatchOperation getDeleteAllBatchOperation(@NonNull MethodInvocationContext context) {
        Class rootEntity = getRequiredRootEntity(context);
        return new DefaultDeleteAllBatchOperation<>(context, rootEntity);
    }

    /**
     * Get the delete batch operation for the given context.
     *
     * @param context  The context
     * @param iterable The iterable
     * @param       The entity type
     * @return The paged query
     */
    @NonNull
    protected  DeleteBatchOperation getDeleteBatchOperation(@NonNull MethodInvocationContext context, @NonNull Iterable iterable) {
        Class rootEntity = getRequiredRootEntity(context);
        return getDeleteBatchOperation(context, rootEntity, iterable);
    }

    /**
     * Get the delete returning batch operation for the given context.
     *
     * @param context  The context
     * @param iterable The iterable
     * @param       The entity type
     * @param       The result type
     * @return The paged query
     */
    @Experimental
    @NonNull
    protected  DeleteReturningBatchOperation getDeleteReturningBatchOperation(@NonNull MethodInvocationContext context, @NonNull Iterable iterable) {
        Class rootEntity = getRequiredRootEntity(context);
        return new DefaultDeleteReturningBatchOperation<>(context, rootEntity, iterable);
    }

    /**
     * Get the delete batch operation for the given context.
     *
     * @param         The entity type
     * @param context    The context
     * @param rootEntity The root entity
     * @param iterable   The iterable
     * @return The paged query
     */
    @NonNull
    protected  DeleteBatchOperation getDeleteBatchOperation(@NonNull MethodInvocationContext context, Class rootEntity, @NonNull Iterable iterable) {
        return new DefaultDeleteBatchOperation<>(context, rootEntity, iterable);
    }

    /**
     * Get the batch operation for the given context.
     *
     * @param context The context
     * @param entity  The entity
     * @param      The entity type
     * @return The paged query
     */
    @NonNull
    protected  InsertOperation getInsertOperation(@NonNull MethodInvocationContext context, E entity) {
        return new DefaultInsertOperation<>(context, entity);
    }

    /**
     * Validates null arguments ensuring no argument is null unless declared so.
     *
     * @param context The context
     */
    protected final void validateNullArguments(MethodInvocationContext context) {
        Object[] parameterValues = context.getParameterValues();
        for (int i = 0; i < parameterValues.length; i++) {
            Object o = parameterValues[i];
            if (o == null && !context.getArguments()[i].isNullable()) {
                throw new IllegalArgumentException("Argument [" + context.getArguments()[i].getName() + "] value is null and the method parameter is not declared as nullable");
            }
        }
    }

    /**
     * Count the items.
     *
     * @param iterable the iterable
     * @return the size
     */
    protected int count(Iterable iterable) {
        if (iterable instanceof Collection collection) {
            return collection.size();
        }
        Iterator iterator = iterable.iterator();
        int i = 0;
        while (iterator.hasNext()) {
            i++;
            iterator.next();
        }
        return i;
    }

    /**
     * Is the type a number.
     *
     * @param type The type
     * @return True if is a number
     */
    protected boolean isNumber(@Nullable Class type) {
        if (type == null) {
            return false;
        }
        if (type.isPrimitive()) {
            return ClassUtils.getPrimitiveType(type.getName()).map(aClass ->
                    Number.class.isAssignableFrom(ReflectionUtils.getWrapperType(aClass))
            ).orElse(false);
        }
        return Number.class.isAssignableFrom(type);
    }

    /**
     * Sets the property value for given persistent property of the {@link BeanWrapper} if property
     * present in given parameter values and property not readonly or generated.
     *
     * @param wrapper the bean wrapper
     * @param prop the persistent property
     * @param parameterValues the parameter value map
     */
    private static void setProperty(BeanWrapper wrapper, PersistentProperty prop, Map parameterValues) {
        if (!prop.isReadOnly() && !prop.isGenerated()) {
            String propName = prop.getName();
            if (parameterValues.containsKey(propName)) {

                Object v = parameterValues.get(propName);
                if (v == null && !prop.isOptional()) {
                    throw new IllegalArgumentException("Argument [" + propName + "] cannot be null");
                }
                wrapper.setProperty(propName, v);
            } else if (prop.isRequired()) {
                final Optional p = wrapper.getProperty(propName, Object.class);
                if (!p.isPresent()) {
                    throw new IllegalArgumentException("Argument [" + propName + "] cannot be null");
                }
            }
        }
    }

    /**
     * Default implementation of {@link InsertOperation}.
     *
     * @param  The entity type
     */
    private final class DefaultInsertOperation extends AbstractEntityOperation implements InsertOperation {
        private final E entity;

        DefaultInsertOperation(MethodInvocationContext method, E entity) {
            super(method, (Class) entity.getClass());
            this.entity = entity;
        }

        @Override
        public E getEntity() {
            return entity;
        }

    }

    /**
     * Default implementation of {@link DeleteOperation}.
     *
     * @param  The entity type
     */
    private final class DefaultDeleteOperation extends AbstractEntityInstanceOperation implements DeleteOperation {
        DefaultDeleteOperation(MethodInvocationContext method, E entity) {
            super(method, entity);
        }
    }

    /**
     * Default implementation of {@link DeleteReturningOperation}.
     *
     * @param  The entity type
     * @param  The result type
     */
    private final class DefaultDeleteReturningOperation extends AbstractEntityInstanceOperation implements DeleteReturningOperation {
        DefaultDeleteReturningOperation(MethodInvocationContext method, E entity) {
            super(method, entity);
        }

        @Override
        public StoredQuery getStoredQuery() {
            return (StoredQuery) super.getStoredQuery();
        }
    }

    /**
     * Default implementation of {@link DeleteReturningBatchOperation}.
     *
     * @param  The entity type
     * @param  The result type
     */
    private final class DefaultDeleteReturningBatchOperation extends DefaultDeleteBatchOperation implements DeleteReturningBatchOperation {

        DefaultDeleteReturningBatchOperation(MethodInvocationContext method, @NonNull Class rootEntity, Iterable iterable) {
            super(method, rootEntity, iterable);
        }

        @Override
        public StoredQuery getStoredQuery() {
            return (StoredQuery) super.getStoredQuery();
        }
    }

    /**
     * Default implementation of {@link UpdateOperation}.
     *
     * @param  The entity type
     */
    private final class DefaultUpdateOperation extends AbstractEntityOperation implements UpdateOperation {
        private final E entity;

        DefaultUpdateOperation(MethodInvocationContext method, E entity) {
            super(method, (Class) entity.getClass());
            this.entity = entity;
        }

        @Override
        public E getEntity() {
            return entity;
        }

    }

    private abstract sealed class AbstractEntityInstanceOperation extends AbstractEntityOperation implements EntityInstanceOperation {
        private final E entity;

        AbstractEntityInstanceOperation(MethodInvocationContext method, E entity) {
            super(method, (Class) entity.getClass());
            this.entity = entity;
        }

        @NonNull
        @Override
        public E getEntity() {
            return entity;
        }

    }

    private abstract sealed class AbstractEntityOperation extends AbstractPreparedDataOperation implements EntityOperation {
        protected final MethodInvocationContext method;
        protected final Class rootEntity;
        protected StoredQuery storedQuery;

        AbstractEntityOperation(MethodInvocationContext method, Class rootEntity) {
            super((MethodInvocationContext) method, new DefaultStoredDataOperation<>(method.getExecutableMethod()));
            this.method = method;
            this.rootEntity = rootEntity;
        }

        @Override
        public StoredQuery getStoredQuery() {
            if (storedQuery == null) {
                String queryString = method.stringValue(Query.class).orElse(null);
                if (queryString == null) {
                    return null;
                }
                storedQuery = findStoreQuery(method);
            }
            return storedQuery;
        }

        @Override
        public  Optional getParameterInRole(@NonNull String role, @NonNull Class type) {
            return AbstractQueryInterceptor.this.getParameterInRole(method, role, type);
        }

        @NonNull
        @Override
        public Class getRootEntity() {
            return rootEntity;
        }

        @NonNull
        @Override
        public Class getRepositoryType() {
            return method.getTarget().getClass();
        }

        @NonNull
        @Override
        public String getName() {
            return method.getDeclaringType().getSimpleName() + "." + method.getMethodName();
        }

        @Override
        public InvocationContext getInvocationContext() {
            return method;
        }
    }

    /**
     * Default implementation of {@link InsertBatchOperation}.
     *
     * @param  The entity type
     */
    private final class DefaultInsertBatchOperation extends DefaultBatchOperation implements InsertBatchOperation {
        DefaultInsertBatchOperation(MethodInvocationContext method, @NonNull Class rootEntity, Iterable iterable) {
            super(method, rootEntity, iterable);
        }

        @Override
        public List> split() {
            List> inserts = new ArrayList<>(10);
            for (E e : iterable) {
                inserts.add(new DefaultInsertOperation<>(method, e));
            }
            return inserts;
        }
    }

    /**
     * Default implementation of {@link DeleteBatchOperation}.
     *
     * @param  The entity type
     */
    private final class DefaultDeleteAllBatchOperation extends DefaultBatchOperation implements DeleteBatchOperation {

        DefaultDeleteAllBatchOperation(MethodInvocationContext method, @NonNull Class rootEntity) {
            super(method, rootEntity, Collections.emptyList());
        }

        @Override
        public boolean all() {
            return true;
        }

        @Override
        public List> split() {
            throw new IllegalStateException("Split is not supported for delete all operation!");
        }
    }

    /**
     * Default implementation of {@link DeleteBatchOperation}.
     *
     * @param  The entity type
     */
    private sealed class DefaultDeleteBatchOperation extends DefaultBatchOperation implements DeleteBatchOperation {

        DefaultDeleteBatchOperation(MethodInvocationContext method, @NonNull Class rootEntity, Iterable iterable) {
            super(method, rootEntity, iterable);
        }

        @Override
        public List> split() {
            List> deletes = new ArrayList<>(10);
            for (E e : iterable) {
                deletes.add(new DefaultDeleteOperation<>(method, e));
            }
            return deletes;
        }

    }

    /**
     * Default implementation of {@link UpdateBatchOperation}.
     *
     * @param  The entity type
     */
    private final class DefaultUpdateBatchOperation extends DefaultBatchOperation implements UpdateBatchOperation {

        DefaultUpdateBatchOperation(MethodInvocationContext method, @NonNull Class rootEntity, Iterable iterable) {
            super(method, rootEntity, iterable);
        }

        @Override
        public List> split() {
            List> updates = new ArrayList<>(10);
            for (E e : iterable) {
                updates.add(new DefaultUpdateOperation<>(method, e));
            }
            return updates;
        }

    }

    /**
     * Default implementation of {@link BatchOperation}.
     *
     * @param  The entity type
     */
    private sealed class DefaultBatchOperation extends AbstractEntityOperation implements BatchOperation {
        protected final Iterable iterable;

        public DefaultBatchOperation(MethodInvocationContext method, @NonNull Class rootEntity, Iterable iterable) {
            super(method, rootEntity);
            this.iterable = iterable;
        }

        @Override
        public Iterator iterator() {
            return iterable.iterator();
        }

    }

}