BAEL-696 Implement OR in the REST API Query Language - Alternate Impl (#1576)

* Dependency Injection Types, XML-Config, Java-Config, Test Classes

* Formatting done with Formatter Configuration in Eclipse

* REST Query Lang - Adv Search Ops - Improvement - C1

* REST Query Lang - Adv Search Ops - Improvement - C2

* BAEL-696 Code formatting

* REST Query Lang - Adv Search Ops - Improvement - C3

* BAEL-696 Formatting

* OR operation with PostFix Expression

* Revert the changes done for PostFix Expr

* Merged from Upstream

* Remove Sorting of Predicates

* REST Query Lang - Adv Search Ops - Improvement - C5
This commit is contained in:
ahamedm 2017-04-02 22:57:45 +04:00 committed by pedja4
parent c3b73c5e56
commit d361c91ed3
8 changed files with 233 additions and 48 deletions

View File

@ -1,7 +1,9 @@
package org.baeldung.persistence.dao;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -11,7 +13,7 @@ import org.baeldung.web.util.SpecSearchCriteria;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.domain.Specifications;
public class GenericSpecificationsBuilder {
public class GenericSpecificationsBuilder<U> {
private final List<SpecSearchCriteria> params;
@ -19,11 +21,11 @@ public class GenericSpecificationsBuilder {
this.params = new ArrayList<>();
}
public final GenericSpecificationsBuilder with(final String key, final String operation, final Object value, final String prefix, final String suffix) {
public final GenericSpecificationsBuilder<U> with(final String key, final String operation, final Object value, final String prefix, final String suffix) {
return with(null, key, operation, value, prefix, suffix);
}
public final GenericSpecificationsBuilder with(final String precedenceIndicator, final String key, final String operation, final Object value, final String prefix, final String suffix) {
public final GenericSpecificationsBuilder<U> with(final String precedenceIndicator, final String key, final String operation, final Object value, final String prefix, final String suffix) {
SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0));
if (op != null) {
if (op == SearchOperation.EQUALITY) // the operation may be complex operation
@ -44,33 +46,54 @@ public class GenericSpecificationsBuilder {
return this;
}
public <U> Specification<U> build(Function<SpecSearchCriteria, Specification<U>> converter) {
public Specification<U> build(Function<SpecSearchCriteria, Specification<U>> converter) {
if (params.size() == 0) {
return null;
}
params.sort(Comparator.comparing(SpecSearchCriteria::isOrPredicate));
final List<Specification<U>> specs = params
.stream()
.map(converter)
.collect(Collectors.toCollection(ArrayList::new));
final List<Specification<U>> specs = params.stream()
.map(converter)
.collect(Collectors.toCollection(ArrayList::new));
Specification<U> result = specs.get(0);
for (int idx = 1; idx < specs.size(); idx++) {
result = params
.get(idx)
.isOrPredicate()
? Specifications
.where(result)
.or(specs.get(idx))
: Specifications
.where(result)
.and(specs.get(idx));
result = params.get(idx)
.isOrPredicate()
? Specifications.where(result)
.or(specs.get(idx))
: Specifications.where(result)
.and(specs.get(idx));
}
return result;
}
public Specification<U> build(Deque<?> postFixedExprStack, Function<SpecSearchCriteria, Specification<U>> converter) {
Deque<Specification<U>> specStack = new LinkedList<>();
Collections.reverse((List<?>) postFixedExprStack);
while (!postFixedExprStack.isEmpty()) {
Object mayBeOperand = postFixedExprStack.pop();
if (!(mayBeOperand instanceof String)) {
specStack.push(converter.apply((SpecSearchCriteria) mayBeOperand));
} else {
Specification<U> operand1 = specStack.pop();
Specification<U> operand2 = specStack.pop();
if (mayBeOperand.equals(SearchOperation.AND_OPERATOR))
specStack.push(Specifications.where(operand1)
.and(operand2));
else if (mayBeOperand.equals(SearchOperation.OR_OPERATOR))
specStack.push(Specifications.where(operand1)
.or(operand2));
}
}
return specStack.pop();
}
}

View File

@ -1,7 +1,6 @@
package org.baeldung.persistence.dao;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.baeldung.persistence.model.User;
@ -24,7 +23,7 @@ public final class UserSpecificationsBuilder {
return with(null, key, operation, value, prefix, suffix);
}
public final UserSpecificationsBuilder with(final String precedenceIndicator, final String key, final String operation, final Object value, final String prefix, final String suffix) {
public final UserSpecificationsBuilder with(final String orPredicate, final String key, final String operation, final Object value, final String prefix, final String suffix) {
SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0));
if (op != null) {
if (op == SearchOperation.EQUALITY) { // the operation may be complex operation
@ -39,7 +38,7 @@ public final class UserSpecificationsBuilder {
op = SearchOperation.STARTS_WITH;
}
}
params.add(new SpecSearchCriteria(precedenceIndicator, key, op, value));
params.add(new SpecSearchCriteria(orPredicate, key, op, value));
}
return this;
}
@ -49,8 +48,6 @@ public final class UserSpecificationsBuilder {
if (params.size() == 0)
return null;
params.sort(Comparator.comparing(SpecSearchCriteria::isOrPredicate));
Specification<User> result = new UserSpecification(params.get(0));
for (int i = 1; i < params.size(); i++) {

View File

@ -5,14 +5,17 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.baeldung.persistence.dao.GenericSpecificationsBuilder;
import org.baeldung.persistence.dao.IUserDAO;
import org.baeldung.persistence.dao.MyUserPredicatesBuilder;
import org.baeldung.persistence.dao.MyUserRepository;
import org.baeldung.persistence.dao.UserRepository;
import org.baeldung.persistence.dao.UserSpecification;
import org.baeldung.persistence.dao.UserSpecificationsBuilder;
import org.baeldung.persistence.dao.rsql.CustomRsqlVisitor;
import org.baeldung.persistence.model.MyUser;
import org.baeldung.persistence.model.User;
import org.baeldung.web.util.CriteriaParser;
import org.baeldung.web.util.SearchCriteria;
import org.baeldung.web.util.SearchOperation;
import org.springframework.beans.factory.annotation.Autowired;
@ -74,9 +77,8 @@ public class UserController {
@ResponseBody
public List<User> findAllBySpecification(@RequestParam(value = "search") String search) {
UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
String operationSetExper = Joiner
.on("|")
.join(SearchOperation.SIMPLE_OPERATION_SET);
String operationSetExper = Joiner.on("|")
.join(SearchOperation.SIMPLE_OPERATION_SET);
Pattern pattern = Pattern.compile("(\\w+?)(" + operationSetExper + ")(\\p{Punct}?)(\\w+?)(\\p{Punct}?),");
Matcher matcher = pattern.matcher(search + ",");
while (matcher.find()) {
@ -94,12 +96,24 @@ public class UserController {
return dao.findAll(spec);
}
@GetMapping(value = "/users/spec/adv")
@ResponseBody
public List<User> findAllByAdvPredicate(@RequestParam(value = "search") String search) {
Specification<User> spec = resolveSpecificationFromInfixExpr(search);
return dao.findAll(spec);
}
protected Specification<User> resolveSpecificationFromInfixExpr(String searchParameters) {
CriteriaParser parser = new CriteriaParser();
GenericSpecificationsBuilder<User> specBuilder = new GenericSpecificationsBuilder<>();
return specBuilder.build(parser.parse(searchParameters), UserSpecification::new);
}
protected Specification<User> resolveSpecification(String searchParameters) {
UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
String operationSetExper = Joiner
.on("|")
.join(SearchOperation.SIMPLE_OPERATION_SET);
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()) {

View File

@ -0,0 +1,76 @@
package org.baeldung.web.util;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.base.Joiner;
public class CriteriaParser {
private static Map<String, Operator> ops;
private static Pattern SpecCriteraRegex = Pattern.compile("^(\\w+?)(" + Joiner.on("|")
.join(SearchOperation.SIMPLE_OPERATION_SET) + ")(\\p{Punct}?)(\\w+?)(\\p{Punct}?)$");
private enum Operator {
OR(1), AND(2);
final int precedence;
Operator(int p) {
precedence = p;
}
}
static {
Map<String, Operator> tempMap = new HashMap<>();
tempMap.put("AND", Operator.AND);
tempMap.put("OR", Operator.OR);
tempMap.put("or", Operator.OR);
tempMap.put("and", Operator.AND);
ops = Collections.unmodifiableMap(tempMap);
}
private static boolean isHigerPrecedenceOperator(String currOp, String prevOp) {
return (ops.containsKey(prevOp) && ops.get(prevOp).precedence >= ops.get(currOp).precedence);
}
public Deque<?> parse(String searchParam) {
Deque<Object> output = new LinkedList<>();
Deque<String> stack = new LinkedList<>();
for (String token : searchParam.split("\\s+")) {
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;
}
}

View File

@ -9,6 +9,14 @@ public enum SearchOperation {
public static final String ZERO_OR_MORE_REGEX = "*";
public static final String OR_OPERATOR = "OR";
public static final String AND_OPERATOR = "AND";
public static final String LEFT_PARANTHESIS = "(";
public static final String RIGHT_PARANTHESIS = ")";
public static SearchOperation getSimpleOperation(final char input) {
switch (input) {
case ':':

View File

@ -26,6 +26,27 @@ public class SpecSearchCriteria {
this.value = value;
}
public SpecSearchCriteria(String key, String operation, String prefix, String value, String suffix) {
SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0));
if (op != null) {
if (op == SearchOperation.EQUALITY) { // the operation may be complex operation
final boolean startWithAsterisk = prefix != null && prefix.contains(SearchOperation.ZERO_OR_MORE_REGEX);
final boolean endWithAsterisk = suffix != null && suffix.contains(SearchOperation.ZERO_OR_MORE_REGEX);
if (startWithAsterisk && endWithAsterisk) {
op = SearchOperation.CONTAINS;
} else if (startWithAsterisk) {
op = SearchOperation.ENDS_WITH;
} else if (endWithAsterisk) {
op = SearchOperation.STARTS_WITH;
}
}
}
this.key = key;
this.operation = op;
this.value = value;
}
public String getKey() {
return key;
}

View File

@ -6,6 +6,7 @@ import org.baeldung.persistence.dao.UserSpecification;
import org.baeldung.persistence.dao.UserSpecificationsBuilder;
import org.baeldung.persistence.model.User;
import org.baeldung.spring.PersistenceConfig;
import org.baeldung.web.util.CriteriaParser;
import org.baeldung.web.util.SearchOperation;
import org.baeldung.web.util.SpecSearchCriteria;
import org.junit.Before;
@ -82,12 +83,12 @@ public class JPASpecificationIntegrationTest {
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");
SpecSearchCriteria spec = new SpecSearchCriteria("firstName", SearchOperation.EQUALITY, "john");
SpecSearchCriteria spec1 = new SpecSearchCriteria("'","lastName", SearchOperation.EQUALITY, "doe");
List<User> results = repository.findAll(builder
.with(spec1)
.with(spec)
.with(spec1)
.build());
assertThat(results, hasSize(2));
@ -96,11 +97,25 @@ public class JPASpecificationIntegrationTest {
}
@Test
public void givenFirstOrLastNameGenericBuilder_whenGettingListOfUsers_thenCorrect() {
GenericSpecificationsBuilder builder = new GenericSpecificationsBuilder();
public void givenFirstOrLastNameAndAgeGenericBuilder_whenGettingListOfUsers_thenCorrect() {
GenericSpecificationsBuilder<User> builder = new GenericSpecificationsBuilder<>();
Function<SpecSearchCriteria, Specification<User>> converter = UserSpecification::new;
builder.with("'", "firstName", ":", "john", null, null);
builder.with(null, "lastName", ":", "doe", null, null);
CriteriaParser parser=new CriteriaParser();
List<User> results = repository.findAll(builder.build(parser.parse("( lastName:doe OR firstName:john ) AND age:22"), converter));
assertThat(results, hasSize(1));
assertThat(userJohn, isIn(results));
assertThat(userTom, not(isIn(results)));
}
@Test
public void givenFirstOrLastNameGenericBuilder_whenGettingListOfUsers_thenCorrect() {
GenericSpecificationsBuilder<User> builder = new GenericSpecificationsBuilder<>();
Function<SpecSearchCriteria, Specification<User>> converter = UserSpecification::new;
builder.with("firstName", ":", "john", null, null);
builder.with("'", "lastName", ":", "doe", null, null);
List<User> results = repository.findAll(builder.build(converter));

View File

@ -47,8 +47,9 @@ public class JPASpecificationLiveTest {
@Test
public void givenFirstOrLastName_whenGettingListOfUsers_thenCorrect() {
final Response response = givenAuth().get(EURL_PREFIX + "'firstName:john,lastName:doe");
final String result = response.body().asString();
final Response response = givenAuth().get(EURL_PREFIX + "firstName:john,'lastName:doe");
final String result = response.body()
.asString();
assertTrue(result.contains(userJohn.getEmail()));
assertTrue(result.contains(userTom.getEmail()));
}
@ -56,7 +57,8 @@ public class JPASpecificationLiveTest {
@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
final Response response = givenAuth().get(URL_PREFIX + "firstName:john,lastName:doe");
final String result = response.body().asString();
final String result = response.body()
.asString();
assertTrue(result.contains(userJohn.getEmail()));
assertFalse(result.contains(userTom.getEmail()));
@ -65,7 +67,8 @@ public class JPASpecificationLiveTest {
@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
final Response response = givenAuth().get(URL_PREFIX + "firstName!john");
final String result = response.body().asString();
final String result = response.body()
.asString();
assertTrue(result.contains(userTom.getEmail()));
assertFalse(result.contains(userJohn.getEmail()));
@ -74,7 +77,8 @@ public class JPASpecificationLiveTest {
@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
final Response response = givenAuth().get(URL_PREFIX + "age>25");
final String result = response.body().asString();
final String result = response.body()
.asString();
assertTrue(result.contains(userTom.getEmail()));
assertFalse(result.contains(userJohn.getEmail()));
@ -83,7 +87,8 @@ public class JPASpecificationLiveTest {
@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
final Response response = givenAuth().get(URL_PREFIX + "firstName:jo*");
final String result = response.body().asString();
final String result = response.body()
.asString();
assertTrue(result.contains(userJohn.getEmail()));
assertFalse(result.contains(userTom.getEmail()));
@ -92,7 +97,8 @@ public class JPASpecificationLiveTest {
@Test
public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() {
final Response response = givenAuth().get(URL_PREFIX + "firstName:*n");
final String result = response.body().asString();
final String result = response.body()
.asString();
assertTrue(result.contains(userJohn.getEmail()));
assertFalse(result.contains(userTom.getEmail()));
@ -101,7 +107,8 @@ public class JPASpecificationLiveTest {
@Test
public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() {
final Response response = givenAuth().get(URL_PREFIX + "firstName:*oh*");
final String result = response.body().asString();
final String result = response.body()
.asString();
assertTrue(result.contains(userJohn.getEmail()));
assertFalse(result.contains(userTom.getEmail()));
@ -110,13 +117,37 @@ public class JPASpecificationLiveTest {
@Test
public void givenAgeRange_whenGettingListOfUsers_thenCorrect() {
final Response response = givenAuth().get(URL_PREFIX + "age>20,age<25");
final String result = response.body().asString();
final String result = response.body()
.asString();
assertTrue(result.contains(userJohn.getEmail()));
assertFalse(result.contains(userTom.getEmail()));
}
private final String ADV_URL_PREFIX = "http://localhost:8082/spring-security-rest-full/auth/users/spec/adv?search=";
@Test
public void givenFirstOrLastName_whenGettingAdvListOfUsers_thenCorrect() {
final Response response = givenAuth().get(ADV_URL_PREFIX + "firstName:john OR lastName:doe");
final String result = response.body()
.asString();
assertTrue(result.contains(userJohn.getEmail()));
assertTrue(result.contains(userTom.getEmail()));
}
@Test
public void givenFirstOrFirstNameAndAge_whenGettingAdvListOfUsers_thenCorrect() {
final Response response = givenAuth().get(ADV_URL_PREFIX + "( firstName:john OR firstName:tom ) AND age>22");
final String result = response.body()
.asString();
assertFalse(result.contains(userJohn.getEmail()));
assertTrue(result.contains(userTom.getEmail()));
}
private final RequestSpecification givenAuth() {
return RestAssured.given().auth().preemptive().basic("user1", "user1Pass");
return RestAssured.given()
.auth()
.preemptive()
.basic("user1", "user1Pass");
}
}