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

io.micronaut.data.runtime.intercept.criteria.AbstractSpecificationInterceptor Maven / Gradle / Ivy

There is a newer version: 4.10.5
Show newest version
/*
 * Copyright 2017-2021 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.criteria;

import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.data.annotation.RepositoryConfiguration;
import io.micronaut.data.intercept.RepositoryMethodKey;
import io.micronaut.data.model.AssociationUtils;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.Pageable.Mode;
import io.micronaut.data.model.Sort;
import io.micronaut.data.model.jpa.criteria.PersistentEntityFrom;
import io.micronaut.data.model.jpa.criteria.impl.QueryResultPersistentEntityCriteriaQuery;
import io.micronaut.data.model.query.JoinPath;
import io.micronaut.data.model.query.QueryModel;
import io.micronaut.data.model.query.builder.QueryBuilder;
import io.micronaut.data.model.query.builder.QueryResult;
import io.micronaut.data.model.runtime.PreparedQuery;
import io.micronaut.data.model.runtime.StoredQuery;
import io.micronaut.data.operations.BlockingCriteriaCapableRepository;
import io.micronaut.data.operations.CriteriaRepositoryOperations;
import io.micronaut.data.operations.RepositoryOperations;
import io.micronaut.data.repository.jpa.criteria.CriteriaDeleteBuilder;
import io.micronaut.data.repository.jpa.criteria.CriteriaQueryBuilder;
import io.micronaut.data.repository.jpa.criteria.CriteriaUpdateBuilder;
import io.micronaut.data.repository.jpa.criteria.DeleteSpecification;
import io.micronaut.data.repository.jpa.criteria.PredicateSpecification;
import io.micronaut.data.repository.jpa.criteria.QuerySpecification;
import io.micronaut.data.repository.jpa.criteria.UpdateSpecification;
import io.micronaut.data.runtime.criteria.RuntimeCriteriaBuilder;
import io.micronaut.data.runtime.intercept.AbstractQueryInterceptor;
import io.micronaut.data.runtime.query.MethodContextAwareStoredQueryDecorator;
import io.micronaut.data.runtime.query.PreparedQueryDecorator;
import io.micronaut.data.runtime.query.StoredQueryDecorator;
import io.micronaut.data.runtime.query.internal.QueryResultStoredQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.CriteriaUpdate;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Order;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

import static io.micronaut.data.model.runtime.StoredQuery.OperationType;

/**
 * Abstract specification interceptor.
 *
 * @param  The declaring type
 * @param  The return type
 * @author Denis Stepanov
 * @since 3.2
 */
@Internal
public abstract class AbstractSpecificationInterceptor extends AbstractQueryInterceptor {

    protected final CriteriaRepositoryOperations criteriaRepositoryOperations;
    private final Map sqlQueryBuilderForRepositories = new ConcurrentHashMap<>();
    private final Map> methodsJoinPaths = new ConcurrentHashMap<>();
    private final CriteriaBuilder criteriaBuilder;
    private final MethodContextAwareStoredQueryDecorator storedQueryDecorator;
    private final PreparedQueryDecorator preparedQueryDecorator;

    /**
     * Default constructor.
     *
     * @param operations The operations
     */
    protected AbstractSpecificationInterceptor(RepositoryOperations operations) {
        super(operations);
        if (operations instanceof CriteriaRepositoryOperations criteriaOps) {
            criteriaRepositoryOperations = criteriaOps;
            criteriaBuilder = criteriaRepositoryOperations.getCriteriaBuilder();
        } else if (operations instanceof BlockingCriteriaCapableRepository repository) {
            criteriaRepositoryOperations = repository.blocking();
            criteriaBuilder = criteriaRepositoryOperations.getCriteriaBuilder();
        } else {
            criteriaRepositoryOperations = null;
            criteriaBuilder = operations.getApplicationContext().getBean(RuntimeCriteriaBuilder.class);
        }
        if (operations instanceof MethodContextAwareStoredQueryDecorator) {
            storedQueryDecorator = (MethodContextAwareStoredQueryDecorator) operations;
        } 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;
                }
            };
        }
        preparedQueryDecorator = operations instanceof PreparedQueryDecorator decorator ? decorator : new PreparedQueryDecorator() {
            @Override
            public  PreparedQuery decorate(PreparedQuery preparedQuery) {
                return preparedQuery;
            }
        };
    }

    @NonNull
    protected final Iterable findAll(RepositoryMethodKey methodKey, MethodInvocationContext context, Type type) {
        Set methodJoinPaths = getMethodJoinPaths(methodKey, context);
        if (criteriaRepositoryOperations != null) {
            CriteriaQuery query = buildQuery(context, type, methodJoinPaths);
            Pageable pageable = getPageable(context);
            if (pageable != null) {
                if (pageable.getMode() != Mode.OFFSET) {
                    throw new UnsupportedOperationException("Pageable mode " + pageable.getMode() + " is not supported with specifications");
                }
                return criteriaRepositoryOperations.findAll(query, (int) pageable.getOffset(), pageable.getSize());
            }
            return criteriaRepositoryOperations.findAll(query);
        }
        return operations.findAll(preparedQueryForCriteria(methodKey, context, type, methodJoinPaths));
    }

    @NonNull
    protected final Object findOne(RepositoryMethodKey methodKey, MethodInvocationContext context, Type type) {
        Set methodJoinPaths = getMethodJoinPaths(methodKey, context);
        if (criteriaRepositoryOperations != null) {
            return criteriaRepositoryOperations.findOne(buildQuery(context, type, methodJoinPaths));
        }
        return operations.findOne(preparedQueryForCriteria(methodKey, context, type, methodJoinPaths));
    }

    protected final Set getMethodJoinPaths(RepositoryMethodKey methodKey, MethodInvocationContext context) {
        return methodsJoinPaths.computeIfAbsent(methodKey, repositoryMethodKey ->
            AssociationUtils.getJoinFetchPaths(context));
    }

    @NonNull
    protected final Long count(RepositoryMethodKey methodKey, MethodInvocationContext context) {
        Set methodJoinPaths = getMethodJoinPaths(methodKey, context);
        Long count;
        if (criteriaRepositoryOperations != null) {
            count = criteriaRepositoryOperations.findOne(buildCountQuery(context));
        } else {
            count = operations.findOne(preparedQueryForCriteria(methodKey, context, Type.COUNT, methodJoinPaths));
        }
        return count == null ? 0 : count;
    }

    protected final boolean exists(RepositoryMethodKey methodKey, MethodInvocationContext context) {
        Set methodJoinPaths = getMethodJoinPaths(methodKey, context);
        if (criteriaRepositoryOperations != null) {
            return criteriaRepositoryOperations.findOne(buildExistsQuery(context, methodJoinPaths));
        }
        Object one = operations.findOne(preparedQueryForCriteria(methodKey, context, Type.EXISTS, methodJoinPaths));
        return one instanceof Boolean aBoolean ? aBoolean : one != null;
    }

    protected final Optional deleteAll(RepositoryMethodKey methodKey, MethodInvocationContext context) {
        Set methodJoinPaths = getMethodJoinPaths(methodKey, context);
        if (criteriaRepositoryOperations != null) {
            return criteriaRepositoryOperations.deleteAll(buildDeleteQuery(context));
        }
        return operations.executeDelete(preparedQueryForCriteria(methodKey, context, Type.DELETE_ALL, methodJoinPaths));
    }

    protected final Optional updateAll(RepositoryMethodKey methodKey, MethodInvocationContext context) {
        Set methodJoinPaths = getMethodJoinPaths(methodKey, context);
        if (criteriaRepositoryOperations != null) {
            return criteriaRepositoryOperations.updateAll(buildUpdateQuery(context));
        }
        return operations.executeUpdate(preparedQueryForCriteria(methodKey, context, Type.UPDATE_ALL, methodJoinPaths));
    }

    @NonNull
    protected final  PreparedQuery preparedQueryForCriteria(RepositoryMethodKey methodKey,
                                                                          MethodInvocationContext context,
                                                                          Type type,
                                                                          Set methodJoinPaths) {

        Pageable pageable = findPageable(context);
        QueryBuilder sqlQueryBuilder = getQueryBuilder(methodKey, context);
        StoredQuery storedQuery;
        if (type == Type.FIND_ALL || type == Type.FIND_ONE || type == Type.FIND_PAGE) {
            storedQuery = buildFind(methodKey, context, type, methodJoinPaths);
        } else if (type == Type.COUNT) {
            storedQuery = buildCount(methodKey, context);
        } else if (type == Type.DELETE_ALL) {
            storedQuery = buildDeleteAll(context, sqlQueryBuilder);
        } else if (type == Type.UPDATE_ALL) {
            storedQuery = buildUpdateAll(context, sqlQueryBuilder);
        } else if (type == Type.EXISTS) {
            storedQuery = buildExists(context, sqlQueryBuilder, methodJoinPaths);
        } else {
            throw new IllegalStateException("Unknown criteria type: " + type);
        }
        storedQuery = storedQueryDecorator.decorate(context, storedQuery);
        PreparedQuery preparedQuery = (PreparedQuery) preparedQueryResolver.resolveQuery(context, storedQuery, pageable);
        return preparedQueryDecorator.decorate(preparedQuery);
    }

    @NonNull
    private Pageable findPageable(MethodInvocationContext context) {
        Pageable pageable = Pageable.UNPAGED;
        for (Object param : context.getParameterValues()) {
            if (param instanceof Pageable pageableParam) {
                pageable = pageableParam;
                break;
            }
        }
        return pageable;
    }

    @NonNull
    private QueryBuilder getQueryBuilder(RepositoryMethodKey methodKey, MethodInvocationContext context) {
        return sqlQueryBuilderForRepositories.computeIfAbsent(methodKey, repositoryMethodKey -> {
                Class builder = context.getAnnotationMetadata().classValue(RepositoryConfiguration.class, "queryBuilder")
                    .orElseThrow(() -> new IllegalStateException("Cannot determine QueryBuilder"));
                BeanIntrospection introspection = BeanIntrospection.getIntrospection(builder);
                if (introspection.getConstructorArguments().length == 1
                    && introspection.getConstructorArguments()[0].getType() == AnnotationMetadata.class) {
                    return introspection.instantiate(context.getAnnotationMetadata());
                }
                return introspection.instantiate();
            }
        );
    }

    private  StoredQuery buildExists(MethodInvocationContext context, QueryBuilder sqlQueryBuilder, Set annotationJoinPaths) {
        CriteriaQuery criteriaQuery = buildExistsQuery(context, annotationJoinPaths);
        QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) criteriaQuery).buildQuery(sqlQueryBuilder);

        return QueryResultStoredQuery.single(OperationType.EXISTS, context.getName(), context.getAnnotationMetadata(),
            queryResult, getRequiredRootEntity(context));
    }

    protected final  CriteriaQuery buildExistsQuery(MethodInvocationContext context, Set annotationJoinPaths) {
        return this.getCriteriaQueryBuilder(context, annotationJoinPaths).build(criteriaBuilder);
    }

    private  StoredQuery buildUpdateAll(MethodInvocationContext context, QueryBuilder sqlQueryBuilder) {
        CriteriaUpdate criteriaUpdate = buildUpdateQuery(context);
        QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) criteriaUpdate).buildQuery(sqlQueryBuilder);
        return QueryResultStoredQuery.single(OperationType.UPDATE, context.getName(),
            context.getAnnotationMetadata(), queryResult, (Class) criteriaUpdate.getRoot().getJavaType());
    }

    protected final  CriteriaUpdate buildUpdateQuery(MethodInvocationContext context) {
        return this.getCriteriaUpdateBuilder(context).build(criteriaBuilder);
    }

    private  StoredQuery buildDeleteAll(MethodInvocationContext context, QueryBuilder sqlQueryBuilder) {
        CriteriaDelete criteriaDelete = buildDeleteQuery(context);
        QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) criteriaDelete).buildQuery(sqlQueryBuilder);
        return QueryResultStoredQuery.single(OperationType.DELETE, context.getName(),
            context.getAnnotationMetadata(), queryResult, (Class) criteriaDelete.getRoot().getJavaType());
    }

    protected final  CriteriaDelete buildDeleteQuery(MethodInvocationContext context) {
        return this.getCriteriaDeleteBuilder(context).build(criteriaBuilder);
    }

    private  StoredQuery buildCount(RepositoryMethodKey methodKey,
                                             MethodInvocationContext context) {
        CriteriaQuery criteriaQuery = buildCountQuery(context);
        QueryBuilder sqlQueryBuilder = getQueryBuilder(methodKey, context);
        QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) criteriaQuery).buildQuery(sqlQueryBuilder);
        return QueryResultStoredQuery.count(context.getName(), context.getAnnotationMetadata(), queryResult, getRequiredRootEntity(context));
    }

    @NonNull
    protected final  CriteriaQuery buildCountQuery(MethodInvocationContext context) {
        Class rootEntity = getRequiredRootEntity(context);
        QuerySpecification specification = getQuerySpecification(context);
        CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Long.class);
        Root root = criteriaQuery.from(rootEntity);
        if (specification != null) {
            Predicate predicate = specification.toPredicate(root, criteriaQuery, criteriaBuilder);
            if (predicate != null) {
                criteriaQuery.where(predicate);
            }
        }
        if (criteriaQuery.isDistinct()) {
            return criteriaQuery.select(criteriaBuilder.countDistinct(root));
        } else {
            return criteriaQuery.select(criteriaBuilder.count(root));
        }
    }

    private  StoredQuery buildFind(RepositoryMethodKey methodKey,
                                                 MethodInvocationContext context,
                                                 Type type,
                                                 Set methodJoinPaths) {

        CriteriaQuery criteriaQuery = buildInternalQuery(context, type, methodJoinPaths);
        QueryBuilder sqlQueryBuilder = getQueryBuilder(methodKey, context);
        QueryResultPersistentEntityCriteriaQuery queryModelCriteriaQuery = (QueryResultPersistentEntityCriteriaQuery) criteriaQuery;
        QueryModel queryModel = queryModelCriteriaQuery.getQueryModel();
        Collection queryJoinPaths = queryModel.getJoinPaths();
        QueryResult queryResult = sqlQueryBuilder.buildQuery(AnnotationMetadata.EMPTY_METADATA, queryModel);
        Set joinPaths = mergeJoinPaths(methodJoinPaths, queryJoinPaths);
        Class rootEntity = getRequiredRootEntity(context);
        if (type == Type.FIND_ONE) {
            return QueryResultStoredQuery.single(OperationType.QUERY, context.getName(), context.getAnnotationMetadata(),
                queryResult, rootEntity, criteriaQuery.getResultType(), joinPaths);
        }
        Pageable pageable = findPageable(context);
        return QueryResultStoredQuery.many(context.getName(), context.getAnnotationMetadata(), queryResult, rootEntity,
            criteriaQuery.getResultType(), !pageable.isUnpaged(), joinPaths);
    }

    private  CriteriaQuery buildInternalQuery(MethodInvocationContext context, Type type, Set methodJoinPaths) {
        CriteriaQueryBuilder builder = getCriteriaQueryBuilder(context, methodJoinPaths);
        CriteriaQuery criteriaQuery = builder.build(criteriaBuilder);

        if (type == Type.FIND_ALL) {
            Pageable pageable = findPageable(context);
            for (Object param : context.getParameterValues()) {
                if (param instanceof Sort sort && param != pageable) {
                    if (sort.isSorted()) {
                        Root root = criteriaQuery.getRoots().stream().findFirst().orElseThrow(() -> new IllegalStateException("The root not found!"));
                        criteriaQuery.orderBy(getOrders(sort, root, criteriaBuilder));
                        break;
                    }
                }
            }
        }
        return criteriaQuery;
    }

    protected final  CriteriaQuery buildQuery(MethodInvocationContext context, Type type, Set methodJoinPaths) {
        CriteriaQueryBuilder builder = getCriteriaQueryBuilder(context, methodJoinPaths);
        CriteriaQuery criteriaQuery = builder.build(criteriaBuilder);

        for (Object param : context.getParameterValues()) {
            if (param instanceof Sort sort) {
                if (sort.isSorted()) {
                    Root root = criteriaQuery.getRoots().stream().findFirst().orElseThrow(() -> new IllegalStateException("The root not found!"));
                    criteriaQuery.orderBy(getOrders(sort, root, criteriaBuilder));
                    break;
                }
            }
        }
        return criteriaQuery;
    }

    /**
     * Find {@link io.micronaut.data.repository.jpa.criteria.QuerySpecification} in context.
     *
     * @param context The context
     * @param      the specification entity root type
     * @return found specification
     */
    @Nullable
    protected  QuerySpecification getQuerySpecification(MethodInvocationContext context) {
        final Object parameterValue = context.getParameterValues()[0];
        if (parameterValue instanceof QuerySpecification querySpecification) {
            return querySpecification;
        }
        if (parameterValue instanceof PredicateSpecification predicateSpecification) {
            return QuerySpecification.where(predicateSpecification);
        }
        Argument parameterArgument = context.getArguments()[0];
        if (parameterArgument.isAssignableFrom(QuerySpecification.class) || parameterArgument.isAssignableFrom(PredicateSpecification.class)) {
            return null;
        }
        throw new IllegalArgumentException("Argument must be an instance of: " + QuerySpecification.class + " or " + PredicateSpecification.class);
    }

    /**
     * Find {@link io.micronaut.data.repository.jpa.criteria.CriteriaQueryBuilder}
     * or {@link io.micronaut.data.repository.jpa.criteria.QuerySpecification} in context.
     *
     * @param context   The context
     * @param joinPaths The join fetch paths
     * @param        the result type
     * @return found specification
     */
    @NonNull
    protected  CriteriaQueryBuilder getCriteriaQueryBuilder(MethodInvocationContext context, Set joinPaths) {
        final Object parameterValue = context.getParameterValues()[0];
        if (parameterValue instanceof CriteriaQueryBuilder criteriaQueryBuilder) {
            return criteriaQueryBuilder;
        }
        return criteriaBuilder -> {
            Class rootEntity = getRequiredRootEntity(context);
            QuerySpecification specification = getQuerySpecification(context);
            CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(rootEntity);
            Root root = criteriaQuery.from(rootEntity);
            if (specification != null) {
                Predicate predicate = specification.toPredicate(root, criteriaQuery, criteriaBuilder);
                if (predicate != null) {
                    criteriaQuery.where(predicate);
                }
            }
            if (CollectionUtils.isNotEmpty(joinPaths)) {
                for (JoinPath joinPath : joinPaths) {
                    join(root, joinPath);
                }
            }
            return criteriaQuery;
        };
    }

    private void join(Root root, JoinPath joinPath) {
        if (root instanceof PersistentEntityFrom persistentEntityFrom) {
            Optional optAlias = joinPath.getAlias();
            if (optAlias.isPresent()) {
                persistentEntityFrom.join(joinPath.getPath(), joinPath.getJoinType(), optAlias.get());
            } else {
                persistentEntityFrom.join(joinPath.getPath(), joinPath.getJoinType());
            }
        }
    }

    private Set mergeJoinPaths(Collection joinPaths, Collection additionalJoinPaths) {
        Set resultPaths = CollectionUtils.newHashSet(joinPaths.size() + additionalJoinPaths.size());
        if (CollectionUtils.isNotEmpty(joinPaths)) {
            resultPaths.addAll(joinPaths);
        }
        if (CollectionUtils.isNotEmpty(additionalJoinPaths)) {
            Map existingPathsByPath = resultPaths.stream().collect(Collectors.toMap(JoinPath::getPath, Function.identity()));
            resultPaths.addAll(additionalJoinPaths.stream().filter(jp -> !existingPathsByPath.containsKey(jp.getPath())).collect(Collectors.toSet()));
        }
        return resultPaths;
    }

    /**
     * Find {@link io.micronaut.data.repository.jpa.criteria.DeleteSpecification} in context.
     *
     * @param context The context
     * @param      the specification entity root type
     * @return found specification
     */
    @Nullable
    protected  DeleteSpecification getDeleteSpecification(MethodInvocationContext context) {
        final Object parameterValue = context.getParameterValues()[0];
        if (parameterValue instanceof DeleteSpecification deleteSpecification) {
            return deleteSpecification;
        }
        if (parameterValue instanceof PredicateSpecification predicateSpecification) {
            return DeleteSpecification.where(predicateSpecification);
        }
        Argument parameterArgument = context.getArguments()[0];
        if (parameterArgument.isAssignableFrom(DeleteSpecification.class) || parameterArgument.isAssignableFrom(PredicateSpecification.class)) {
            return null;
        }
        throw new IllegalArgumentException("Argument must be an instance of: " + DeleteSpecification.class + " or " + PredicateSpecification.class);
    }

    /**
     * Find {@link io.micronaut.data.repository.jpa.criteria.CriteriaDeleteBuilder}
     * or {@link io.micronaut.data.repository.jpa.criteria.QuerySpecification} in context.
     *
     * @param context The context
     * @param      the result type
     * @return found specification
     */
    @NonNull
    protected  CriteriaDeleteBuilder getCriteriaDeleteBuilder(MethodInvocationContext context) {
        final Object parameterValue = context.getParameterValues()[0];
        if (parameterValue instanceof CriteriaDeleteBuilder criteriaDeleteBuilder) {
            return criteriaDeleteBuilder;
        }
        return criteriaBuilder -> {
            Class rootEntity = getRequiredRootEntity(context);
            DeleteSpecification specification = getDeleteSpecification(context);
            CriteriaDelete criteriaDelete = criteriaBuilder.createCriteriaDelete(rootEntity);
            Root root = criteriaDelete.from(rootEntity);
            if (specification != null) {
                Predicate predicate = specification.toPredicate(root, criteriaDelete, criteriaBuilder);
                if (predicate != null) {
                    criteriaDelete.where(predicate);
                }
            }
            return criteriaDelete;
        };
    }

    /**
     * Find {@link io.micronaut.data.repository.jpa.criteria.UpdateSpecification} in context.
     *
     * @param context The context
     * @param      the specification entity root type
     * @return found specification
     */
    @Nullable
    protected  UpdateSpecification getUpdateSpecification(MethodInvocationContext context) {
        final Object parameterValue = context.getParameterValues()[0];
        if (parameterValue instanceof UpdateSpecification updateSpecification) {
            return updateSpecification;
        }
        Argument parameterArgument = context.getArguments()[0];
        if (parameterArgument.isAssignableFrom(UpdateSpecification.class) || parameterArgument.isAssignableFrom(PredicateSpecification.class)) {
            return null;
        }
        throw new IllegalArgumentException("Argument must be an instance of: " + UpdateSpecification.class);
    }

    /**
     * Find {@link io.micronaut.data.repository.jpa.criteria.CriteriaUpdateBuilder}
     * or {@link io.micronaut.data.repository.jpa.criteria.QuerySpecification} in context.
     *
     * @param context The context
     * @param      the result type
     * @return found specification
     */
    @NonNull
    protected  CriteriaUpdateBuilder getCriteriaUpdateBuilder(MethodInvocationContext context) {
        final Object parameterValue = context.getParameterValues()[0];
        if (parameterValue instanceof CriteriaUpdateBuilder criteriaUpdateBuilder) {
            return criteriaUpdateBuilder;
        }
        return criteriaBuilder -> {
            Class rootEntity = getRequiredRootEntity(context);
            UpdateSpecification specification = getUpdateSpecification(context);
            CriteriaUpdate criteriaUpdate = criteriaBuilder.createCriteriaUpdate(rootEntity);
            Root root = criteriaUpdate.from(rootEntity);
            if (specification != null) {
                Predicate predicate = specification.toPredicate(root, criteriaUpdate, criteriaBuilder);
                if (predicate != null) {
                    criteriaUpdate.where(predicate);
                }
            }
            return criteriaUpdate;
        };
    }

    private List getOrders(Sort sort, Root root, CriteriaBuilder cb) {
        List orders = new ArrayList<>();
        for (Sort.Order order : sort.getOrderBy()) {
            Path path = root;
            for (String property : StringUtils.splitOmitEmptyStrings(order.getProperty(), '.')) {
                path = path.get(property);
            }
            Expression expression = order.isIgnoreCase() ? cb.lower(path.type().as(String.class)) : path;
            orders.add(order.isAscending() ? cb.asc(expression) : cb.desc(expression));
        }
        return orders;
    }

    protected enum Type {
        COUNT, FIND_ONE, FIND_PAGE, FIND_ALL, DELETE_ALL, UPDATE_ALL, EXISTS
    }

}