From d361c91ed3f9e2216c441f61be820b19f0748dfa Mon Sep 17 00:00:00 2001 From: ahamedm Date: Sun, 2 Apr 2017 22:57:45 +0400 Subject: [PATCH] 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 --- .../dao/GenericSpecificationsBuilder.java | 63 ++++++++++----- .../dao/UserSpecificationsBuilder.java | 7 +- .../web/controller/UserController.java | 26 +++++-- .../org/baeldung/web/util/CriteriaParser.java | 76 +++++++++++++++++++ .../baeldung/web/util/SearchOperation.java | 8 ++ .../baeldung/web/util/SpecSearchCriteria.java | 21 +++++ .../JPASpecificationIntegrationTest.java | 29 +++++-- .../query/JPASpecificationLiveTest.java | 51 ++++++++++--- 8 files changed, 233 insertions(+), 48 deletions(-) create mode 100644 spring-security-rest-full/src/main/java/org/baeldung/web/util/CriteriaParser.java diff --git a/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/GenericSpecificationsBuilder.java b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/GenericSpecificationsBuilder.java index 45c015f233..64bab9a435 100644 --- a/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/GenericSpecificationsBuilder.java +++ b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/GenericSpecificationsBuilder.java @@ -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 { private final List 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 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 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 Specification build(Function> converter) { + public Specification build(Function> converter) { if (params.size() == 0) { return null; } - params.sort(Comparator.comparing(SpecSearchCriteria::isOrPredicate)); - - final List> specs = params - .stream() - .map(converter) - .collect(Collectors.toCollection(ArrayList::new)); + final List> specs = params.stream() + .map(converter) + .collect(Collectors.toCollection(ArrayList::new)); Specification 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 build(Deque postFixedExprStack, Function> converter) { + + Deque> 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 operand1 = specStack.pop(); + Specification 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(); + + } + } diff --git a/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/UserSpecificationsBuilder.java b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/UserSpecificationsBuilder.java index bbcb521241..a8e5b96acb 100644 --- a/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/UserSpecificationsBuilder.java +++ b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/UserSpecificationsBuilder.java @@ -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 result = new UserSpecification(params.get(0)); for (int i = 1; i < params.size(); i++) { diff --git a/spring-security-rest-full/src/main/java/org/baeldung/web/controller/UserController.java b/spring-security-rest-full/src/main/java/org/baeldung/web/controller/UserController.java index 4c21d9836d..8953a52a1b 100644 --- a/spring-security-rest-full/src/main/java/org/baeldung/web/controller/UserController.java +++ b/spring-security-rest-full/src/main/java/org/baeldung/web/controller/UserController.java @@ -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 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 findAllByAdvPredicate(@RequestParam(value = "search") 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); + } + protected Specification 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()) { diff --git a/spring-security-rest-full/src/main/java/org/baeldung/web/util/CriteriaParser.java b/spring-security-rest-full/src/main/java/org/baeldung/web/util/CriteriaParser.java new file mode 100644 index 0000000000..eabc938bce --- /dev/null +++ b/spring-security-rest-full/src/main/java/org/baeldung/web/util/CriteriaParser.java @@ -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 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 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 output = new LinkedList<>(); + Deque 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; + } + +} diff --git a/spring-security-rest-full/src/main/java/org/baeldung/web/util/SearchOperation.java b/spring-security-rest-full/src/main/java/org/baeldung/web/util/SearchOperation.java index fa09662201..db2c0133cf 100644 --- a/spring-security-rest-full/src/main/java/org/baeldung/web/util/SearchOperation.java +++ b/spring-security-rest-full/src/main/java/org/baeldung/web/util/SearchOperation.java @@ -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 ':': diff --git a/spring-security-rest-full/src/main/java/org/baeldung/web/util/SpecSearchCriteria.java b/spring-security-rest-full/src/main/java/org/baeldung/web/util/SpecSearchCriteria.java index 6b37fb579c..3435ff3342 100644 --- a/spring-security-rest-full/src/main/java/org/baeldung/web/util/SpecSearchCriteria.java +++ b/spring-security-rest-full/src/main/java/org/baeldung/web/util/SpecSearchCriteria.java @@ -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; } diff --git a/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/JPASpecificationIntegrationTest.java b/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/JPASpecificationIntegrationTest.java index 244e19db90..d9ae95c876 100644 --- a/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/JPASpecificationIntegrationTest.java +++ b/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/JPASpecificationIntegrationTest.java @@ -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 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 builder = new GenericSpecificationsBuilder<>(); Function> converter = UserSpecification::new; - builder.with("'", "firstName", ":", "john", null, null); - builder.with(null, "lastName", ":", "doe", null, null); + + CriteriaParser parser=new CriteriaParser(); + List 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 builder = new GenericSpecificationsBuilder<>(); + Function> converter = UserSpecification::new; + + builder.with("firstName", ":", "john", null, null); + builder.with("'", "lastName", ":", "doe", null, null); List results = repository.findAll(builder.build(converter)); diff --git a/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/JPASpecificationLiveTest.java b/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/JPASpecificationLiveTest.java index 55fde80add..70787266d8 100644 --- a/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/JPASpecificationLiveTest.java +++ b/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/JPASpecificationLiveTest.java @@ -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"); } }