├── .gitignore ├── src └── main │ └── java │ └── net │ └── kaczmarzyk │ └── example │ ├── web │ ├── HelloController.java │ ├── NotDeletedCustomerSpecification.java │ ├── CustomerByNameSpecification.java │ └── CustomerController.java │ ├── repo │ └── CustomerRepository.java │ ├── Application.java │ ├── domain │ └── Customer.java │ └── DataInit.java ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.project 3 | /.settings/ 4 | /.classpath 5 | -------------------------------------------------------------------------------- /src/main/java/net/kaczmarzyk/example/web/HelloController.java: -------------------------------------------------------------------------------- 1 | package net.kaczmarzyk.example.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | 6 | @Controller 7 | public class HelloController { 8 | 9 | @RequestMapping("") 10 | public String hello() { 11 | return "redirect:/customers"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/kaczmarzyk/example/repo/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package net.kaczmarzyk.example.repo; 2 | 3 | import net.kaczmarzyk.example.domain.Customer; 4 | 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | import org.springframework.data.repository.PagingAndSortingRepository; 7 | 8 | public interface CustomerRepository extends PagingAndSortingRepository, JpaSpecificationExecutor { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/net/kaczmarzyk/example/web/NotDeletedCustomerSpecification.java: -------------------------------------------------------------------------------- 1 | package net.kaczmarzyk.example.web; 2 | 3 | import org.springframework.data.jpa.domain.Specification; 4 | 5 | import net.kaczmarzyk.example.domain.Customer; 6 | import net.kaczmarzyk.spring.data.jpa.domain.Equal; 7 | import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec; 8 | 9 | @Spec(path = "deleted", constVal = "false", spec = Equal.class) 10 | public interface NotDeletedCustomerSpecification extends Specification { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/net/kaczmarzyk/example/web/CustomerByNameSpecification.java: -------------------------------------------------------------------------------- 1 | package net.kaczmarzyk.example.web; 2 | 3 | import net.kaczmarzyk.spring.data.jpa.domain.LikeIgnoreCase; 4 | import net.kaczmarzyk.spring.data.jpa.web.annotation.Or; 5 | import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec; 6 | 7 | 8 | @Or({ 9 | @Spec(params="name", path="firstName", spec=LikeIgnoreCase.class), 10 | @Spec(params="name", path="lastName", spec=LikeIgnoreCase.class) 11 | }) 12 | public interface CustomerByNameSpecification extends NotDeletedCustomerSpecification { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | some examples for SpecificationArgumentResolver 2 | =============================================== 3 | 4 | This is a simple Spring Boot project which presents [Specification Argument Resolver library](https://github.com/tkaczmarzyk/specification-arg-resolver). 5 | 6 | It is an executable jar with embedded H2 db, so just build it with Maven and explore the API. You will find some samples below: 7 | 8 | 1. Get all customers: 9 | 10 | curl http://localhost:8080/customers 11 | 12 | 2. Delete a customer 13 | 14 | curl -X DELETE http://localhost:8080/customers/3 15 | 16 | it will perform a soft delete, which you can verify by getting all customers again -- the deleted customer will be still there, but with `deleted = true` flag. As you will see in subsequent points, specification-based query methods will filter it out. 17 | 18 | 3. Filter customers (include only not deleted ones) by first name and gender (optionally): 19 | 20 | curl http://localhost:8080/customers?firstName=ar 21 | curl 'http://localhost:8080/customers?firstName=ar&gender=MALE' 22 | 23 | 4. Filter by name (either first or last, case insensitive) with paging: 24 | 25 | curl 'http://localhost:8080/customers?name=l&page=0&size=2&sort=id' 26 | -------------------------------------------------------------------------------- /src/main/java/net/kaczmarzyk/example/Application.java: -------------------------------------------------------------------------------- 1 | package net.kaczmarzyk.example; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.SpringBootConfiguration; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.context.annotation.ComponentScan; 9 | import org.springframework.data.web.PageableHandlerMethodArgumentResolver; 10 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 12 | 13 | import net.kaczmarzyk.spring.data.jpa.web.SpecificationArgumentResolver; 14 | 15 | 16 | @SpringBootConfiguration 17 | @ComponentScan(basePackages="net.kaczmarzyk") 18 | @EnableAutoConfiguration 19 | public class Application implements WebMvcConfigurer { 20 | 21 | @Override 22 | public void addArgumentResolvers(List argumentResolvers) { 23 | argumentResolvers.add(new SpecificationArgumentResolver()); 24 | argumentResolvers.add(new PageableHandlerMethodArgumentResolver()); 25 | } 26 | 27 | public static void main(String[] args) throws Exception { 28 | SpringApplication.run(Application.class, args); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/net/kaczmarzyk/example/domain/Customer.java: -------------------------------------------------------------------------------- 1 | package net.kaczmarzyk.example.domain; 2 | 3 | import java.util.Date; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.Id; 8 | 9 | import com.fasterxml.jackson.annotation.JsonFormat; 10 | 11 | @Entity 12 | public class Customer { 13 | 14 | @Id @GeneratedValue 15 | private Long id; 16 | 17 | private String gender; 18 | 19 | private String firstName; 20 | 21 | private String lastName; 22 | 23 | @JsonFormat(pattern="yyyy-MM-dd") 24 | private Date registrationDate; 25 | 26 | private boolean deleted; 27 | 28 | 29 | public Customer() { 30 | } 31 | 32 | public Customer(String firstName, String lastName, String gender, Date registrationDate) { 33 | this.firstName = firstName; 34 | this.lastName = lastName; 35 | this.gender = gender; 36 | this.registrationDate = registrationDate; 37 | } 38 | 39 | public Long getId() { 40 | return id; 41 | } 42 | 43 | public String getFirstName() { 44 | return firstName; 45 | } 46 | 47 | public String getLastName() { 48 | return lastName; 49 | } 50 | 51 | public String getGender() { 52 | return gender; 53 | } 54 | 55 | public Date getRegistrationDate() { 56 | return registrationDate; 57 | } 58 | 59 | public void delete() { 60 | this.deleted = true; 61 | } 62 | 63 | public boolean isDeleted() { 64 | return deleted; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/net/kaczmarzyk/example/DataInit.java: -------------------------------------------------------------------------------- 1 | package net.kaczmarzyk.example; 2 | 3 | import java.util.Calendar; 4 | import java.util.Date; 5 | 6 | import javax.annotation.PostConstruct; 7 | import javax.persistence.EntityManager; 8 | import javax.persistence.PersistenceContext; 9 | 10 | import net.kaczmarzyk.example.domain.Customer; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.transaction.PlatformTransactionManager; 15 | import org.springframework.transaction.TransactionStatus; 16 | import org.springframework.transaction.support.TransactionCallbackWithoutResult; 17 | import org.springframework.transaction.support.TransactionTemplate; 18 | 19 | 20 | @Component 21 | public class DataInit { 22 | 23 | @PersistenceContext 24 | private EntityManager em; 25 | 26 | @Autowired 27 | private PlatformTransactionManager txManager; 28 | 29 | 30 | @PostConstruct 31 | public void initializeData() { 32 | new TransactionTemplate(txManager).execute(new TransactionCallbackWithoutResult() { 33 | @Override 34 | protected void doInTransactionWithoutResult(TransactionStatus arg0) { 35 | em.persist(new Customer("Homer", "Simpson", "M", date(2014, Calendar.JANUARY, 01))); 36 | em.persist(new Customer("Marge", "Simpson", "F", date(2014, Calendar.JANUARY, 07))); 37 | em.persist(new Customer("Bart", "Simpson", "M", date(2014, Calendar.JANUARY, 14))); 38 | em.persist(new Customer("Lisa", "Simpson", "F", date(2014, Calendar.JANUARY, 21))); 39 | em.persist(new Customer("Barney", "Gumble", "M", date(2014, Calendar.JANUARY, 22))); 40 | em.persist(new Customer("Moe", "Szyslak", "M", date(2014, Calendar.JANUARY, 23))); 41 | } 42 | }); 43 | } 44 | 45 | private Date date(int year, int month, int day) { 46 | Calendar cal = Calendar.getInstance(); 47 | cal.set(Calendar.YEAR, year); 48 | cal.set(Calendar.MONTH, month); 49 | cal.set(Calendar.DAY_OF_MONTH, day); 50 | return cal.getTime(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | net.kaczmarzyk 5 | specification-argument-resolver-example 6 | 2.0-SNAPSHOT 7 | jar 8 | 9 | specification-argument-resolver-example 10 | http://maven.apache.org 11 | 12 | 13 | UTF-8 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-parent 19 | 2.0.0.M6 20 | 21 | 22 | 23 | 24 | spring-milestones 25 | Spring Milestones 26 | http://repo.spring.io/milestone 27 | 28 | false 29 | 30 | 31 | 32 | kaczmarzyk.net 33 | http://repo.kaczmarzyk.net 34 | 35 | 36 | 37 | 38 | 39 | spring-milestones 40 | Spring Milestones 41 | http://repo.spring.io/milestone 42 | 43 | false 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | maven-compiler-plugin 52 | 53 | 1.8 54 | 1.8 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-maven-plugin 60 | 61 | 62 | 63 | repackage 64 | 65 | 66 | 67 | 68 | ZIP 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | org.springframework.boot 77 | spring-boot-starter-web 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-starter-data-jpa 82 | 83 | 84 | com.h2database 85 | h2 86 | 87 | 88 | net.kaczmarzyk 89 | specification-arg-resolver 90 | 1.1.1 91 | 92 | 93 | 94 | junit 95 | junit 96 | test 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/main/java/net/kaczmarzyk/example/web/CustomerController.java: -------------------------------------------------------------------------------- 1 | package net.kaczmarzyk.example.web; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.domain.Specification; 6 | import org.springframework.data.jpa.domain.Specifications; 7 | import org.springframework.data.web.PageableDefault; 8 | import org.springframework.web.bind.annotation.DeleteMapping; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import net.kaczmarzyk.example.domain.Customer; 15 | import net.kaczmarzyk.example.repo.CustomerRepository; 16 | import net.kaczmarzyk.spring.data.jpa.domain.DateBefore; 17 | import net.kaczmarzyk.spring.data.jpa.domain.DateBetween; 18 | import net.kaczmarzyk.spring.data.jpa.domain.Like; 19 | import net.kaczmarzyk.spring.data.jpa.domain.LikeIgnoreCase; 20 | import net.kaczmarzyk.spring.data.jpa.web.annotation.And; 21 | import net.kaczmarzyk.spring.data.jpa.web.annotation.Or; 22 | import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec; 23 | 24 | 25 | @RestController 26 | @RequestMapping("/customers") 27 | public class CustomerController { 28 | 29 | @Autowired 30 | CustomerRepository customerRepo; 31 | 32 | @RequestMapping 33 | public Iterable listAllCustomers() { 34 | return customerRepo.findAll(); 35 | } 36 | 37 | @DeleteMapping("/{customerId}") 38 | public void deleteCustomer(@PathVariable("customerId") Long customerId) { 39 | Customer customer = customerRepo.findById(customerId).orElseThrow(() -> new IllegalArgumentException("customer does not exist!")); 40 | customer.delete(); 41 | customerRepo.save(customer); 42 | } 43 | 44 | @GetMapping(params = { "firstName" }) 45 | public Iterable filterCustomersByFirstName( 46 | @Spec(path = "firstName", spec = Like.class) NotDeletedCustomerSpecification spec) { 47 | 48 | return customerRepo.findAll(spec); 49 | } 50 | 51 | @GetMapping(params = { "lastName" }) // gender param is optional 52 | public Iterable filterCustomersByLastNameAndGender( 53 | @And({@Spec(path = "lastName", spec = Like.class), 54 | @Spec(path = "gender", spec = Like.class)}) Specification spec) { 55 | 56 | return customerRepo.findAll(spec); 57 | } 58 | 59 | @GetMapping(params = { "name" }) 60 | public Iterable filterCustomersByNameWithPaging( 61 | CustomerByNameSpecification spec, 62 | @PageableDefault(size = 1, sort = "id") Pageable pageable) { 63 | 64 | return customerRepo.findAll(spec, pageable); 65 | } 66 | 67 | @GetMapping(params = { "registeredBefore" }) 68 | public Iterable findCustomersRegisteredBefore( 69 | @Spec(path = "registrationDate", params = "registeredBefore", config = "yyyy-MM-dd", spec = DateBefore.class) NotDeletedCustomerSpecification spec) { 70 | 71 | return customerRepo.findAll(spec); 72 | } 73 | 74 | @GetMapping(params = { "registeredAfter", "registeredBefore" }) 75 | public Iterable findCustomersByRegistrationDate( 76 | @Spec(path = "registrationDate", params = {"registeredAfter", "registeredBefore"}, config = "yyyy-MM-dd", spec = DateBetween.class) NotDeletedCustomerSpecification spec) { 77 | 78 | return customerRepo.findAll(spec); 79 | } 80 | 81 | @GetMapping(params = { "gender", "name" }) 82 | public Iterable findCustomersByGenderAndName( 83 | @Spec(path = "gender", spec = Like.class) CustomerByNameSpecification spec) { 84 | 85 | return customerRepo.findAll(spec); 86 | } 87 | 88 | @GetMapping(value = "", params = { "registeredBefore", "name" }) 89 | public Iterable findCustomersByRegistrationDateAndName( 90 | @Spec(path="registrationDate", params="registeredBefore", spec=DateBefore.class) NotDeletedCustomerSpecification registrationDateSpec, 91 | @Or({@Spec(params="name", path="firstName", spec=Like.class), 92 | @Spec(params="name", path="lastName", spec=Like.class)}) NotDeletedCustomerSpecification nameSpec) { 93 | 94 | Specification spec = Specifications.where(registrationDateSpec).and(nameSpec); 95 | 96 | return customerRepo.findAll(spec); 97 | } 98 | } 99 | --------------------------------------------------------------------------------