diff --git a/pom.xml b/pom.xml index 09935fc..9bc5c33 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ jar demo - + org.springframework.boot @@ -22,9 +22,39 @@ UTF-8 UTF-8 1.8 + 4.1.3 + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.data + spring-data-commons + + + + com.querydsl + querydsl-apt + ${querydsl.version} + provided + + + + com.querydsl + querydsl-jpa + ${querydsl.version} + + + + org.slf4j + slf4j-log4j12 + 1.6.1 + + + com.h2database h2 @@ -39,10 +69,7 @@ org.springframework.boot spring-boot-starter-data-jpa - - org.springframework.boot - spring-boot-starter-web - + mysql @@ -63,13 +90,71 @@ google-collections 1.0 + + + io.rest-assured + rest-assured + 3.0.0 + test + + + io.rest-assured.examples + scala-example + 3.0.0 + test + + + - org.springframework.boot - spring-boot-maven-plugin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.mysema.maven + apt-maven-plugin + 1.1.3 + + + + process + + + target/generated-sources/java + com.querydsl.apt.jpa.JPAAnnotationProcessor + + + diff --git a/src/main/java/com/example/demo/persistence/dao/MyUserPredicate.java b/src/main/java/com/example/demo/persistence/dao/MyUserPredicate.java new file mode 100644 index 0000000..248af3d --- /dev/null +++ b/src/main/java/com/example/demo/persistence/dao/MyUserPredicate.java @@ -0,0 +1,57 @@ +package com.example.demo.persistence.dao; + +import com.example.demo.persistence.model.MyUser; +import com.example.demo.web.util.SearchCriteria; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.PathBuilder; +import com.querydsl.core.types.dsl.StringPath; + +public class MyUserPredicate { + + private SearchCriteria criteria; + + public MyUserPredicate(final SearchCriteria criteria) { + this.criteria = criteria; + } + + public BooleanExpression getPredicate() { + final PathBuilder entityPath = new PathBuilder<>(MyUser.class, "myUser"); + + if (isNumeric(criteria.getValue().toString())) { + final NumberPath path = entityPath.getNumber(criteria.getKey(), Integer.class); + final int value = Integer.parseInt(criteria.getValue().toString()); + switch (criteria.getOperation()) { + case ":": + return path.eq(value); + case ">": + return path.goe(value); + case "<": + return path.loe(value); + } + } else { + final StringPath path = entityPath.getString(criteria.getKey()); + if (criteria.getOperation().equalsIgnoreCase(":")) { + return path.containsIgnoreCase(criteria.getValue().toString()); + } + } + return null; + } + + public SearchCriteria getCriteria() { + return criteria; + } + + public void setCriteria(final SearchCriteria criteria) { + this.criteria = criteria; + } + + public static boolean isNumeric(final String str) { + try { + Integer.parseInt(str); + } catch (final NumberFormatException e) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/persistence/dao/MyUserPredicateBuilder.java b/src/main/java/com/example/demo/persistence/dao/MyUserPredicateBuilder.java new file mode 100644 index 0000000..4fc7a11 --- /dev/null +++ b/src/main/java/com/example/demo/persistence/dao/MyUserPredicateBuilder.java @@ -0,0 +1,42 @@ +package com.example.demo.persistence.dao; + +import com.example.demo.web.util.SearchCriteria; +import com.querydsl.core.types.dsl.BooleanExpression; + +import java.util.ArrayList; +import java.util.List; + +public final class MyUserPredicateBuilder { + private final List params; + + public MyUserPredicateBuilder() { + params = new ArrayList<>(); + } + + public MyUserPredicateBuilder with(final String key, final String operation, final Object value) { + params.add(new SearchCriteria(key, operation, value)); + return this; + } + + public BooleanExpression build() { + if (params.size() == 0) { + return null; + } + + final List predicates = new ArrayList<>(); + MyUserPredicate predicate; + for (final SearchCriteria param : params) { + predicate = new MyUserPredicate(param); + final BooleanExpression exp = predicate.getPredicate(); + if (exp != null) { + predicates.add(exp); + } + } + + BooleanExpression result = predicates.get(0); + for (int i = 1; i < predicates.size(); i++) { + result = result.and(predicates.get(i)); + } + return result; + } +} diff --git a/src/main/java/com/example/demo/persistence/dao/repository/MyUserRepository.java b/src/main/java/com/example/demo/persistence/dao/repository/MyUserRepository.java new file mode 100644 index 0000000..623433c --- /dev/null +++ b/src/main/java/com/example/demo/persistence/dao/repository/MyUserRepository.java @@ -0,0 +1,26 @@ +package com.example.demo.persistence.dao.repository; + +import com.example.demo.persistence.model.MyUser; +import com.example.demo.persistence.model.QMyUser; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.core.types.dsl.StringPath; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QueryDslPredicateExecutor; +import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; +import org.springframework.data.querydsl.binding.QuerydslBindings; +import com.querydsl.core.types.dsl.StringExpression; + +import com.querydsl.core.types.dsl.StringPath; +import org.springframework.data.querydsl.binding.SingleValueBinding; + +public interface MyUserRepository extends JpaRepository, + QueryDslPredicateExecutor, QuerydslBinderCustomizer { + + @Override + default void customize(final QuerydslBindings bindings, final QMyUser root) { + bindings.bind(String.class) + .first((SingleValueBinding) StringExpression::containsIgnoreCase); + bindings.excluding(root.email); + } +} + diff --git a/src/main/java/com/example/demo/persistence/model/MyUser.java b/src/main/java/com/example/demo/persistence/model/MyUser.java new file mode 100644 index 0000000..c74b51d --- /dev/null +++ b/src/main/java/com/example/demo/persistence/model/MyUser.java @@ -0,0 +1,31 @@ +package com.example.demo.persistence.model; + +import lombok.*; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +@EqualsAndHashCode +public class MyUser { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private int id; + + private String firstName; + + private String lastName; + + private String email; + + private int age; +} diff --git a/src/main/java/com/example/demo/persistence/model/User.java b/src/main/java/com/example/demo/persistence/model/User.java index 94f046f..9c89dbe 100644 --- a/src/main/java/com/example/demo/persistence/model/User.java +++ b/src/main/java/com/example/demo/persistence/model/User.java @@ -14,6 +14,8 @@ @Builder @AllArgsConstructor @NoArgsConstructor +@ToString +@EqualsAndHashCode public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) diff --git a/src/main/java/com/example/demo/web/controller/UserController.java b/src/main/java/com/example/demo/web/controller/UserController.java index 8a7894e..9c2d945 100644 --- a/src/main/java/com/example/demo/web/controller/UserController.java +++ b/src/main/java/com/example/demo/web/controller/UserController.java @@ -1,15 +1,20 @@ package com.example.demo.web.controller; import com.example.demo.persistence.dao.IUserDao; +import com.example.demo.persistence.dao.MyUserPredicateBuilder; import com.example.demo.persistence.dao.UserSpecificationBuilder; +import com.example.demo.persistence.dao.repository.MyUserRepository; import com.example.demo.persistence.dao.repository.UserRepository; +import com.example.demo.persistence.model.MyUser; import com.example.demo.persistence.model.User; import com.example.demo.web.util.SearchCriteria; +import com.querydsl.core.types.dsl.BooleanExpression; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.domain.Specification; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.data.querydsl.binding.QuerydslPredicate; +import org.springframework.web.bind.annotation.*; +import com.querydsl.core.types.Predicate; + import java.util.ArrayList; import java.util.List; @@ -21,10 +26,12 @@ public class UserController { private final IUserDao api; private final UserRepository repo; + private final MyUserRepository myUserRepository; @Autowired - public UserController(IUserDao api, UserRepository repo) { + public UserController(IUserDao api, UserRepository repo, MyUserRepository myUserRepository) { this.api = api; this.repo = repo; + this.myUserRepository = myUserRepository; } @GetMapping("/users") //users?search=lastName:Bozenka,age>25 @@ -44,7 +51,7 @@ public List findAll(@RequestParam(value = "search", required = false)Strin @GetMapping("/users/specs") //users/specs?search=lastName:Bozenka,age>25 public List search(@RequestParam(value = "search") String search) { UserSpecificationBuilder builder = new UserSpecificationBuilder(); - Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),"); + Pattern pattern = Pattern.compile("(\\w+?)([:<>])(\\w+?),"); Matcher matcher = pattern.matcher(search + ","); while (matcher.find()) { builder.with(matcher.group(1), matcher.group(2), matcher.group(3)); @@ -53,4 +60,25 @@ public List search(@RequestParam(value = "search") String search) { Specification spec = builder.build(); return repo.findAll(spec); } + + @RequestMapping(method = RequestMethod.GET, value = "/myusers") + @ResponseBody//http://localhost:8080/users?search=lastName:doe,age>25 + public Iterable findAllByQuerydsl(@RequestParam(value = "search") String search) { + MyUserPredicateBuilder builder = new MyUserPredicateBuilder(); + if (search != null) { + Pattern pattern = Pattern.compile("(\\w+?)([:<>])(\\w+?),"); + Matcher matcher = pattern.matcher(search + ","); + while (matcher.find()) { + builder.with(matcher.group(1), matcher.group(2), matcher.group(3)); + } + } + BooleanExpression exp = builder.build(); + return myUserRepository.findAll(exp); + } + + @RequestMapping(method = RequestMethod.GET, value = "/api/myusers") + @ResponseBody + public Iterable findAllByWebQuerydsl(@QuerydslPredicate(root = MyUser.class) Predicate predicate) { + return myUserRepository.findAll(predicate); + } } diff --git a/src/test/java/com/example/demo/persistence/dao/MyUserPredicateTest.java b/src/test/java/com/example/demo/persistence/dao/MyUserPredicateTest.java new file mode 100644 index 0000000..93515c8 --- /dev/null +++ b/src/test/java/com/example/demo/persistence/dao/MyUserPredicateTest.java @@ -0,0 +1,78 @@ +package com.example.demo.persistence.dao; + +import com.example.demo.config.PersistenceConfig; +import com.example.demo.persistence.dao.repository.MyUserRepository; +import com.example.demo.persistence.model.MyUser; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.transaction.Transactional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.hamcrest.core.IsNot.not; + + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { PersistenceConfig.class }) +@Transactional +@Rollback +public class MyUserPredicateTest { + + @Autowired + private MyUserRepository repo; + + private MyUser userJohn; + private MyUser userTom; + + @Before + public void setUp() throws Exception { + userJohn = new MyUser(); + userJohn.setFirstName("John"); + userJohn.setLastName("Doe"); + userJohn.setEmail("john@doe.com"); + userJohn.setAge(22); + repo.save(userJohn); + + userTom = new MyUser(); + userTom.setFirstName("Tom"); + userTom.setLastName("Doe"); + userTom.setEmail("tom@doe.com"); + userTom.setAge(26); + repo.save(userTom); + + } + + @Test + public void givenLast_whenGettingListOfUsers_thenCorrect() { + final MyUserPredicateBuilder builder = new MyUserPredicateBuilder().with("lastName", ":", "Doe"); + + final Iterable results = repo.findAll(builder.build()); + assertThat(results, containsInAnyOrder(userJohn, userTom)); + } + + @Test + public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { + final MyUserPredicateBuilder builder = new MyUserPredicateBuilder().with("firstName", ":", "john").with("lastName", ":", "doe"); + + final Iterable results = repo.findAll(builder.build()); + + assertThat(results, contains(userJohn)); + assertThat(results, not(contains(userTom))); + } + + @Test + public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { + final MyUserPredicateBuilder builder = new MyUserPredicateBuilder().with("firstName", ":", "adam").with("lastName", ":", "fox"); + + final Iterable results = repo.findAll(builder.build()); + assertThat(results, emptyIterable()); + } +} \ No newline at end of file diff --git a/src/test/java/com/example/demo/web/controller/MyUserQueryDslWebTest.java b/src/test/java/com/example/demo/web/controller/MyUserQueryDslWebTest.java new file mode 100644 index 0000000..01027eb --- /dev/null +++ b/src/test/java/com/example/demo/web/controller/MyUserQueryDslWebTest.java @@ -0,0 +1,51 @@ +package com.example.demo.web.controller; + +import com.example.demo.persistence.model.MyUser; +import static org.junit.Assert.assertEquals; +import io.restassured.RestAssured; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; + +import org.junit.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +public class MyUserQueryDslWebTest { + + private final MyUser userJohn = new MyUser(1,"john", "doe", "john@doe.com", 11); + private String URL_PREFIX = "http://localhost:8082/spring-rest-query-language/auth/api/myusers"; + + @Test + public void whenGettingListOfUsers_thenCorrect() { + final Response response = givenAuth().get(URL_PREFIX); + final MyUser[] result = response.as(MyUser[].class); + assertEquals(result.length, 2); + } + + @Test + public void givenFirstName_whenGettingListOfUsers_thenCorrect() { + final Response response = givenAuth().get(URL_PREFIX + "?firstName=john"); + final MyUser[] result = response.as(MyUser[].class); + assertEquals(result.length, 1); + assertEquals(result[0].getEmail(), userJohn.getEmail()); + } + + @Test + public void givenPartialLastName_whenGettingListOfUsers_thenCorrect() { + final Response response = givenAuth().get(URL_PREFIX + "?lastName=do"); + final MyUser[] result = response.as(MyUser[].class); + assertEquals(result.length, 2); + } + + @Test + public void givenEmail_whenGettingListOfUsers_thenIgnored() { + final Response response = givenAuth().get(URL_PREFIX + "?email=john"); + final MyUser[] result = response.as(MyUser[].class); + assertEquals(result.length, 2); + } + + private RequestSpecification givenAuth() { + return RestAssured.given().auth().preemptive().basic("user1", "user1Pass"); + } + +} \ No newline at end of file