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

io.lettuce.core.dynamic.DefaultCommandMethodVerifier Maven / Gradle / Ivy

Go to download

Advanced and thread-safe Java Redis client for synchronous, asynchronous, and reactive usage. Supports Cluster, Sentinel, Pipelining, Auto-Reconnect, Codecs and much more.

The newest version!
package io.lettuce.core.dynamic;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import io.lettuce.core.GeoCoordinates;
import io.lettuce.core.KeyValue;
import io.lettuce.core.Limit;
import io.lettuce.core.Range;
import io.lettuce.core.ScoredValue;
import io.lettuce.core.dynamic.parameter.Parameter;
import io.lettuce.core.dynamic.segment.CommandSegment;
import io.lettuce.core.dynamic.segment.CommandSegments;
import io.lettuce.core.internal.LettuceAssert;
import io.lettuce.core.internal.LettuceLists;
import io.lettuce.core.internal.LettuceStrings;
import io.lettuce.core.models.command.CommandDetail;

/**
 * Verifies {@link CommandMethod} declarations by checking available Redis commands.
 *
 * @author Mark Paluch
 * @since 5.0
 */
class DefaultCommandMethodVerifier implements CommandMethodVerifier {

    /**
     * Default maximum property distance: 2
     */
    public static final int DEFAULT_MAX_DISTANCE = 2;

    private List commandDetails;

    /**
     * Create a new {@link DefaultCommandMethodVerifier} given a {@link List} of {@link CommandDetail}
     *
     * @param commandDetails must not be {@code null}.
     */
    public DefaultCommandMethodVerifier(List commandDetails) {

        LettuceAssert.notNull(commandDetails, "Command details must not be null");

        this.commandDetails = LettuceLists.newList(commandDetails);
    }

    /**
     * Verify a {@link CommandMethod} with its {@link CommandSegments}. This method verifies that the command exists and that
     * the required number of arguments is declared.
     *
     * @param commandSegments
     * @param commandMethod
     */
    @Override
    public void validate(CommandSegments commandSegments, CommandMethod commandMethod) throws CommandMethodSyntaxException {

        LettuceAssert.notEmpty(commandSegments.getCommandType().toString(), "Command name must not be empty");

        CommandDetail commandDetail = findCommandDetail(commandSegments.getCommandType().toString())
                .orElseThrow(() -> syntaxException(commandSegments.getCommandType().toString(), commandMethod));

        validateParameters(commandDetail, commandSegments, commandMethod);
    }

    private void validateParameters(CommandDetail commandDetail, CommandSegments commandSegments, CommandMethod commandMethod) {

        List bindableParameters = commandMethod.getParameters().getBindableParameters();

        int availableParameterCount = calculateAvailableParameterCount(commandSegments, bindableParameters);

        // exact parameter count
        if (commandDetail.getArity() - 1 == availableParameterCount) {
            return;
        }

        // more or same parameter cound for dynamic arg count commands
        if (0 > commandDetail.getArity() && availableParameterCount >= -(commandDetail.getArity() + 1)) {
            return;
        }

        for (Parameter bindableParameter : bindableParameters) {

            // Can't verify collection-like arguments as they may contain multiple elements.
            if (bindableParameter.getTypeInformation().isCollectionLike()) {
                return;
            }
        }

        String message;
        if (commandDetail.getArity() == 1) {
            message = String.format("Command %s accepts no parameters.", commandDetail.getName().toUpperCase());
        } else if (commandDetail.getArity() < -1) {
            message = String.format("Command %s requires at least %d parameters but method declares %d parameter(s).",
                    commandDetail.getName().toUpperCase(), Math.abs(commandDetail.getArity()) - 1, availableParameterCount);
        } else {
            message = String.format("Command %s accepts %d parameters but method declares %d parameter(s).",
                    commandDetail.getName().toUpperCase(), commandDetail.getArity() - 1, availableParameterCount);
        }

        throw new CommandMethodSyntaxException(commandMethod, message);
    }

    private int calculateAvailableParameterCount(CommandSegments commandSegments,
            List bindableParameters) {

        int count = commandSegments.size();

        for (int i = 0; i < bindableParameters.size(); i++) {

            Parameter bindableParameter = bindableParameters.get(i);

            boolean consumed = isConsumed(commandSegments, bindableParameter);

            if (consumed) {
                continue;
            }

            if (bindableParameter.isAssignableTo(KeyValue.class) || bindableParameter.isAssignableTo(ScoredValue.class)) {
                count++;
            }

            if (bindableParameter.isAssignableTo(GeoCoordinates.class) || bindableParameter.isAssignableTo(Range.class)) {
                count++;
            }

            if (bindableParameter.isAssignableTo(Limit.class)) {
                count += 2;
            }

            count++;
        }

        return count;
    }

    private boolean isConsumed(CommandSegments commandSegments, Parameter bindableParameter) {

        for (CommandSegment commandSegment : commandSegments) {
            if (commandSegment.canConsume(bindableParameter)) {
                return true;
            }
        }

        return false;
    }

    private CommandMethodSyntaxException syntaxException(String commandName, CommandMethod commandMethod) {

        CommandMatches commandMatches = CommandMatches.forCommand(commandName, commandDetails);

        if (commandMatches.hasMatches()) {
            return new CommandMethodSyntaxException(commandMethod,
                    String.format("Command %s does not exist. Did you mean: %s?", commandName, commandMatches));
        }

        return new CommandMethodSyntaxException(commandMethod, String.format("Command %s does not exist", commandName));

    }

    private Optional findCommandDetail(String commandName) {
        return commandDetails.stream().filter(commandDetail -> commandDetail.getName().equalsIgnoreCase(commandName))
                .findFirst();
    }

    static class CommandMatches {

        private final List matches = new ArrayList<>();

        private CommandMatches(List matches) {
            this.matches.addAll(matches);
        }

        public static CommandMatches forCommand(String command, List commandDetails) {
            return new CommandMatches(calculateMatches(command, commandDetails));
        }

        private static List calculateMatches(String command, List commandDetails) {

            return commandDetails.stream()
                    //
                    .filter(commandDetail -> calculateStringDistance(commandDetail.getName().toLowerCase(),
                            command.toLowerCase()) <= DEFAULT_MAX_DISTANCE)
                    .map(CommandDetail::getName) //
                    .map(String::toUpperCase) //
                    .sorted(CommandMatches::calculateStringDistance).collect(Collectors.toList());
        }

        public boolean hasMatches() {
            return !matches.isEmpty();
        }

        @Override
        public String toString() {
            return LettuceStrings.collectionToDelimitedString(matches, ", ", "", "");
        }

        /**
         * Calculate the distance between the given two Strings according to the Levenshtein algorithm.
         *
         * @param s1 the first String
         * @param s2 the second String
         * @return the distance value
         */
        private static int calculateStringDistance(String s1, String s2) {

            if (s1.length() == 0) {
                return s2.length();
            }

            if (s2.length() == 0) {
                return s1.length();
            }

            int d[][] = new int[s1.length() + 1][s2.length() + 1];

            for (int i = 0; i <= s1.length(); i++) {
                d[i][0] = i;
            }

            for (int j = 0; j <= s2.length(); j++) {
                d[0][j] = j;
            }

            for (int i = 1; i <= s1.length(); i++) {
                char s_i = s1.charAt(i - 1);
                for (int j = 1; j <= s2.length(); j++) {
                    int cost;
                    char t_j = s2.charAt(j - 1);
                    if (s_i == t_j) {
                        cost = 0;
                    } else {
                        cost = 1;
                    }
                    d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + cost);
                }
            }

            return d[s1.length()][s2.length()];
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy