Langage de requête REST - Implémentation de l’opération OR

Langage de requête REST - Implémentation de l'opération OR

1. Vue d'ensemble

Dans cet article rapide, nous allons étendre les opérations de recherche avancée que nous avons implémentées dans lesprevious article et inclure lesOR-based search criteria into our REST API Query Language.

2. Approche de mise en œuvre

Auparavant, tous les critères du paramètre de requêtesearch formaient des prédicats regroupés uniquement par l'opérateur AND. Changeons cela.

Nous devrions pouvoir implémenter cette fonctionnalité soit comme un changement simple et rapide à une approche existante, soit comme une nouvelle approche à partir de zéro.

Avec l'approche simple, nous marquerons les critères pour indiquer qu'il doit être combiné à l'aide de l'opérateur OR.

Par exemple, voici l'URL pour tester l'API pour «firstName OR lastName”:

http://localhost:8080/users?search=firstName:john,'lastName:doe

Notez que nous avons marqué les critèreslastName avec un guillemet simple pour le différencier. Nous capturerons ce prédicat pour l'opérateur OR dans notre objet de valeur de critère -SpecSearchCriteria:

public SpecSearchCriteria(
  String orPredicate, String key, SearchOperation operation, Object value) {
    super();

    this.orPredicate
      = orPredicate != null
      && orPredicate.equals(SearchOperation.OR_PREDICATE_FLAG);

    this.key = key;
    this.operation = operation;
    this.value = value;
}

3. Amélioration deUserSpecificationBuilder

Maintenant, modifions notre générateur de spécifications,UserSpecificationBuilder, pour prendre en compte les critères qualifiés OR lors de la construction deSpecification<User>:

public Specification build() {
    if (params.size() == 0) {
        return null;
    }
    Specification result = new UserSpecification(params.get(0));

    for (int i = 1; i < params.size(); i++) {
        result = params.get(i).isOrPredicate()
          ? Specification.where(result).or(new UserSpecification(params.get(i)))
          : Specification.where(result).and(new UserSpecification(params.get(i)));
    }
    return result;
 }

4. Amélioration deUserController

Enfin, configurons un nouveau point de terminaison REST dans notre contrôleur pour utiliser cette fonctionnalité de recherche avec l'opérateur OR. La logique d'analyse améliorée extrait l'indicateur spécial qui aide à identifier les critères avec l'opérateur OR:

@GetMapping("/users/espec")
@ResponseBody
public List findAllByOrPredicate(@RequestParam String search) {
    Specification spec = resolveSpecification(search);
    return dao.findAll(spec);
}

protected Specification resolveSpecification(String searchParameters) {
    UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
    String operationSetExper = Joiner.on("|")
      .join(SearchOperation.SIMPLE_OPERATION_SET);
    Pattern pattern = Pattern.compile(
      "(\\p{Punct}?)(\\w+?)("
      + operationSetExper
      + ")(\\p{Punct}?)(\\w+?)(\\p{Punct}?),");
    Matcher matcher = pattern.matcher(searchParameters + ",");
    while (matcher.find()) {
        builder.with(matcher.group(1), matcher.group(2), matcher.group(3),
        matcher.group(5), matcher.group(4), matcher.group(6));
    }

    return builder.build();
}

5. Test en direct avec conditionOR

Dans cet exemple de test en direct, avec le nouveau point de terminaison d'API, nous rechercherons les utilisateurs par le prénom «john» OU le nom de famille «doe». Notez que le paramètrelastName a un guillemet simple, ce qui le qualifie de «prédicat OR»:

private String EURL_PREFIX
  = "http://localhost:8082/spring-rest-full/auth/users/espec?search=";

@Test
public void givenFirstOrLastName_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(EURL_PREFIX + "firstName:john,'lastName:doe");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertTrue(result.contains(userTom.getEmail()));
}

6. Test de persistance avec conditionOR

Maintenant, effectuons le même test que nous avons fait ci-dessus, au niveau de persistance pour les utilisateurswith first name “john” OR last name “doe”:

@Test
public void givenFirstOrLastName_whenGettingListOfUsers_thenCorrect() {
    UserSpecificationsBuilder builder = new UserSpecificationsBuilder();

    SpecSearchCriteria spec
      = new SpecSearchCriteria("firstName", SearchOperation.EQUALITY, "john");
    SpecSearchCriteria spec1
      = new SpecSearchCriteria("'","lastName", SearchOperation.EQUALITY, "doe");

    List results = repository
      .findAll(builder.with(spec).with(spec1).build());

    assertThat(results, hasSize(2));
    assertThat(userJohn, isIn(results));
    assertThat(userTom, isIn(results));
}

7. Approche alternative

Dans l'approche alternative, nous pourrions fournir la requête de recherche plus comme une clauseWHERE complète d'une requête SQL.

Par exemple, voici l'URL pour une recherche plus complexe parfirstName etage:

http://localhost:8080/users?search=( firstName:john OR firstName:tom ) AND age>22

Notez que nous avons séparé les critères individuels, les opérateurs et les parenthèses de regroupement avec un espace pour former une expression infixe valide.

Analysons l'expression infixe avec unCriteriaParser. NotreCriteriaParser divise l'expression d'infixe donnée en jetons (critères, parenthèses, opérateurs AND & OR) et crée une expression de suffixe pour la même:

public Deque parse(String searchParam) {

    Deque output = new LinkedList<>();
    Deque stack = new LinkedList<>();

    Arrays.stream(searchParam.split("\\s+")).forEach(token -> {
        if (ops.containsKey(token)) {
            while (!stack.isEmpty() && isHigerPrecedenceOperator(token, stack.peek())) {
                output.push(stack.pop().equalsIgnoreCase(SearchOperation.OR_OPERATOR)
                  ? SearchOperation.OR_OPERATOR : SearchOperation.AND_OPERATOR);
            }
            stack.push(token.equalsIgnoreCase(SearchOperation.OR_OPERATOR)
              ? SearchOperation.OR_OPERATOR : SearchOperation.AND_OPERATOR);

        } else if (token.equals(SearchOperation.LEFT_PARANTHESIS)) {
            stack.push(SearchOperation.LEFT_PARANTHESIS);
        } else if (token.equals(SearchOperation.RIGHT_PARANTHESIS)) {
            while (!stack.peek().equals(SearchOperation.LEFT_PARANTHESIS)) {
                output.push(stack.pop());
            }
            stack.pop();
        } else {
            Matcher matcher = SpecCriteraRegex.matcher(token);
            while (matcher.find()) {
                output.push(new SpecSearchCriteria(
                  matcher.group(1),
                  matcher.group(2),
                  matcher.group(3),
                  matcher.group(4),
                  matcher.group(5)));
            }
        }
    });

    while (!stack.isEmpty()) {
        output.push(stack.pop());
    }

    return output;
}


Ajoutons une nouvelle méthode dans notre générateur de spécifications,GenericSpecificationBuilder, pour construire la rechercheSpecification à partir de l'expression postfix:

    public Specification build(Deque postFixedExprStack,
        Function> converter) {

        Deque> specStack = new LinkedList<>();

        while (!postFixedExprStack.isEmpty()) {
            Object mayBeOperand = postFixedExprStack.pollLast();

            if (!(mayBeOperand instanceof String)) {
                specStack.push(converter.apply((SpecSearchCriteria) mayBeOperand));
            } else {
                Specification operand1 = specStack.pop();
                Specification operand2 = specStack.pop();
                if (mayBeOperand.equals(SearchOperation.AND_OPERATOR)) {
                    specStack.push(Specification.where(operand1)
                      .and(operand2));
                }
                else if (mayBeOperand.equals(SearchOperation.OR_OPERATOR)) {
                    specStack.push(Specification.where(operand1)
                      .or(operand2));
                }
            }
        }
        return specStack.pop();

Enfin, ajoutons un autre point de terminaison REST dans notreUserController pour analyser l'expression complexe avec les nouveauxCriteriaParser:

@GetMapping("/users/spec/adv")
@ResponseBody
public List findAllByAdvPredicate(@RequestParam String search) {
    Specification spec = resolveSpecificationFromInfixExpr(search);
    return dao.findAll(spec);
}

protected Specification resolveSpecificationFromInfixExpr(String searchParameters) {
    CriteriaParser parser = new CriteriaParser();
    GenericSpecificationsBuilder specBuilder = new GenericSpecificationsBuilder<>();
    return specBuilder.build(parser.parse(searchParameters), UserSpecification::new);
}

8. Conclusion

Dans ce didacticiel, nous avons amélioré notre langage de requête REST avec la possibilité de rechercher avec un opérateur OR.

L'implémentation complète de cet article se trouve dansthe GitHub project. Ceci est un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.