├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── nooul │ │ └── apihelpers │ │ └── springbootrest │ │ ├── annotations │ │ └── ValueObject.java │ │ ├── controllerAdvices │ │ ├── BodyAdvice.java │ │ ├── GlobalExceptionAdvice.java │ │ └── ResourceSizeAdvice.java │ │ ├── entities │ │ ├── ErrorDetails.java │ │ └── QueryParamWrapper.java │ │ ├── exceptions │ │ └── NotFoundException.java │ │ ├── providers │ │ └── ObjectMapperProvider.java │ │ ├── repositories │ │ └── BaseRepository.java │ │ ├── serializers │ │ └── IdWrapperSerializer.java │ │ ├── services │ │ └── FilterService.java │ │ ├── specifications │ │ ├── CustomSpecifications.java │ │ └── CustomSpecifications2.java │ │ └── utils │ │ ├── CsvUtils.java │ │ ├── JSONUtils.java │ │ ├── QueryParamExtractor.java │ │ └── UrlUtils.java └── resources │ └── application-test.yaml └── test └── java └── com └── nooul └── apihelpers └── springbootrest ├── AbstractJpaDataTest.java ├── AbstractSpringBootTest.java ├── AbstractUnitTest.java ├── SpringBootRestTest.java ├── TestApp.java ├── helpers ├── controllers │ ├── ActorController.java │ ├── CategoryController.java │ ├── DirectorController.java │ ├── MovieController.java │ ├── SenderController.java │ ├── UUIDEntityController.java │ └── UUIDRelationshipController.java ├── entities │ ├── Actor.java │ ├── Category.java │ ├── Director.java │ ├── Movie.java │ ├── Sender.java │ ├── UUID.java │ ├── UUIDEntity.java │ └── UUIDRelationship.java ├── repositories │ ├── ActorRepository.java │ ├── CategoryRepository.java │ ├── DirectorRepository.java │ ├── MovieRepository.java │ ├── SenderRepository.java │ ├── UUIDEntityRepository.java │ └── UUIDRelationshipRepository.java ├── utils │ └── DateUtils.java └── values │ ├── Mobile.java │ └── MobileConverter.java └── services ├── FilterServiceTest.java └── filterByTest.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | .idea/ 19 | 20 | # External tool builders 21 | .externalToolBuilders/ 22 | 23 | # Locally stored "Eclipse launch configurations" 24 | *.launch 25 | 26 | # CDT-specific 27 | .cproject 28 | 29 | # PDT-specific 30 | .buildpath 31 | 32 | 33 | ################# 34 | ## Visual Studio 35 | ################# 36 | 37 | ## Ignore Visual Studio temporary files, build results, and 38 | ## files generated by popular Visual Studio add-ons. 39 | 40 | # User-specific files 41 | *.suo 42 | *.user 43 | *.sln.docstates 44 | 45 | # Build results 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | *_i.c 49 | *_p.c 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.vspscc 64 | .builds 65 | *.dotCover 66 | 67 | # Visual C++ cache files 68 | ipch/ 69 | *.aps 70 | *.ncb 71 | *.opensdf 72 | *.sdf 73 | 74 | # Visual Studio profiler 75 | *.psess 76 | *.vsp 77 | 78 | # ReSharper is a .NET coding add-in 79 | _ReSharper* 80 | 81 | # Installshield output folder 82 | [Ee]xpress 83 | 84 | # DocProject is a documentation generator add-in 85 | DocProject/buildhelp/ 86 | DocProject/Help/*.HxT 87 | DocProject/Help/*.HxC 88 | DocProject/Help/*.hhc 89 | DocProject/Help/*.hhk 90 | DocProject/Help/*.hhp 91 | DocProject/Help/Html2 92 | DocProject/Help/html 93 | 94 | # Click-Once directory 95 | publish 96 | 97 | # Others 98 | [Bb]in 99 | [Oo]bj 100 | sql 101 | TestResults 102 | *.Cache 103 | ClientBin 104 | stylecop.* 105 | ~$* 106 | *.dbmdl 107 | Generated_Code #added for RIA/Silverlight projects 108 | 109 | # Backup & report files from converting an old project file to a newer Visual Studio version 110 | _UpgradeReport_Files/ 111 | Backup*/ 112 | UpgradeLog*.XML 113 | 114 | 115 | ############ 116 | ## Windows 117 | ############ 118 | 119 | # Windows image file caches 120 | Thumbs.db 121 | 122 | # Folder config file 123 | Desktop.ini 124 | 125 | 126 | ############ 127 | ## Mac 128 | ############ 129 | .DS_Store 130 | 131 | .recommenders 132 | 133 | 134 | ############# 135 | ## Python 136 | ############# 137 | 138 | *.py[co] 139 | 140 | # Packages 141 | *.egg 142 | *.egg-info 143 | dist 144 | build 145 | eggs 146 | parts 147 | bin 148 | var 149 | sdist 150 | develop-eggs 151 | .installed.cfg 152 | 153 | # Installer logs 154 | pip-log.txt 155 | 156 | # Unit test / coverage reports 157 | .coverage 158 | .tox 159 | 160 | #Translations 161 | *.mo 162 | 163 | #Mr Developer 164 | .mr.developer.cfg 165 | 166 | 167 | ############# 168 | ## Java 169 | ############# 170 | 171 | *.class 172 | 173 | 174 | ######################### 175 | ## Other 176 | ######################### 177 | 178 | /target/** 179 | *.iml 180 | .sonar/* 181 | /dumps/** 182 | /data/** 183 | dependency-reduced-pom.xml 184 | .factorypath 185 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Michail Michailidis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-boot-rest-api-helpers 2 | 3 | Java >= 8 (thanks to [davidegironi](https://github.com/davidegironi/)) 4 | 5 | Inspired by built-in fake REST data provider [react-admin](https://github.com/marmelab/react-admin) (see [documentation](https://marmelab.com/react-admin/DataProviders.html)) that queries like that: 6 | ``` 7 | GET /movies?filter={id: 1} //get movies by id = 1 8 | GET /movies?filter={id: [1,2]} // get movies by id = 1 or id = 2 9 | GET /actors?filter={movies: 1, firstName: John} = //actors played in movie with id = 1 and their first name is John 10 | GET /actors?filter={birthYearGt: 1960}&sort=[id,DESC]&range=[0,100] // actors born later than 1960 11 | GET /actors?filter={q: %Keanu Re%} // full text search on all text fields 12 | GET /actors?sort=[firstName,DESC,birthDate,ASC] //sort by multiple fields in case of ties 13 | ``` 14 | More Inspiration was drawn from languages like [FIQL/RSQL](https://github.com/jirutka/rsql-parser) so recently more features were added along with in-memory integration tests, support for non-number primary keys, resulting in a total refactoring of the code and fix of a lot of bugs (there are still some edge cases). 15 | 16 | Now it is possible to also do the following (after url-encode of the query part of the url): 17 | ``` 18 | GET /movies?filter={idNot: 1} //get movies with id not equal to 1 19 | GET /actors?filter={movies: null} = //actors that have played in no movie 20 | GET /actors?filter={moviesNot: null} = //actors that have played to a movie 21 | GET /actors?filter={movies: [1,2]} = //actors played in either movie with id = 1, or movie with id = 2 22 | GET /actors?filter={moviesAnd: [1,2]} = //actors played in both movies with id = 1 and id = 2 23 | GET /actors?filter={moviesNot: [1,2]} = //actors played in neither movie with id = 1, nor movie with id = 2 24 | GET /actors?filter={name: Keanu Ree%} // full text search on specific fields just by the inclusion of one or two '%' in the value 25 | 26 | GET /actors?filter={movies: {name: Matrix}} = //actors that have played in movie with name Matrix 27 | GET /actors?filter={movies: {name: Matrix%}} = //actors that have played in movies with name starting with Matrix 28 | GET /movies?filter={actors: {firstName: Keanu, lastNameNot: Reves}} = //movies with actors that firstName is 'Keanu' but lastName is not 'Reves' 29 | 30 | GET /actors?filter=[{firstName: Keanu},{firstName: John}] = //actors with firstName 'Keanu' or 'John' 31 | GET /actors?filter={firstName: [Keanu, John]} = //equivalent to the above 32 | 33 | GET /documents?filter={uuid: f44010c9-4d3c-45b2-bb6b-6cac8572bb78} // get document with java.util.UUID equal to f44010c9-4d3c-45b2-bb6b-6cac8572bb78 34 | GET /libraries?filter={documents: {uuid: f44010c9-4d3c-45b2-bb6b-6cac8572bb78}} // get libraries that contain document with uuid equal to f44010c9-4d3c-45b2-bb6b-6cac8572bb78 35 | GET /libraries?filter={documents: f44010c9-4d3c-45b2-bb6b-6cac8572bb78} // same as above 36 | 37 | GET /actors?filter={birthDateGt: '1960-01-01'}&sort=[id,DESC]&range=[0,100] // actors born later than 1960-01-01 38 | GET /actors?filter={birthDateGt: '1960-01-01T00:00:00'}&sort=[id,DESC]&range=[0,100] // actors born later than 1960-01-01 00:00:00 (database timezone - UTC recommended) 39 | 40 | ``` 41 | The key names are not the ones on the database but the ones exposed by the REST API and are the names of the entity attribute names. Here `movies` is plural because an Actor has `@ManyToMany` annotation on `List movies` attribute. 42 | 43 | * Keep in mind that key/value pairs that are in { } are combined by default with AND. 44 | ``` 45 | /actors?filter={firstName:'A',lastName:'B'} => firstName = A and lastName = B 46 | ``` 47 | 48 | * Values or Objects that contain key/values in [] are combined by default with OR unless the key in front of the [] is ending with 'And'. 49 | ``` 50 | /actors?filter={movies: [1,2]} => actors having acted at movies with ids 1 OR 2 51 | /movies?filter={actors: [{firstName:'A'}, {lastName:'B'}] } => movies having actors with firstName = A OR lastName = B 52 | /actors?filter={moviesAnd: [1,2]} => actors acted at movies with ids 1 AND 2 53 | /movies?filter={actorsAnd: [{firstName:'A'}, {lastName:'B'}] } => movies having actors with firstName = A AND lastName = B 54 | ``` 55 | 56 | * Disabling distinct search can have some performance boost sometimes - **Warning it will return duplicate entries** 57 | ``` 58 | /actors?filter={movies: 1, firstName: John} 59 | ``` 60 | allowDuplicates is not supported after Hibernate 6 since it always passes distinct:true! 61 | 62 | https://docs.jboss.org/hibernate/orm/6.0/migration-guide/migration-guide.html#query-sqm-distinct 63 | ~~/actors?filter={movies: 1, firstName: John, allowDuplicates: true}~~ 64 | 65 | 66 | * Special case to check if an entity is associated with another entity or is null (only first level) - below is the only syntaxes that will currently work (note replacing `null` with `{id: null}` won't work). The following will bring movies with no director or director with id = 1 or id = 2 67 | ``` 68 | /movies?filter={director: [null,1,2] } 69 | /movies?filter={director: [1,null, 2] } 70 | /movies?filter={director: [{id: 1}.{id: 2},null] } 71 | /movies?filter=[{director: 1},{director: 2},{ director: null}] 72 | /movies?filter=[{director: {id: 1}},{director: {id: 2}}, { director: null}] 73 | ``` 74 | 75 | 76 | **Important**: Keep in mind that the object/array that is passed in filter needs to be url encoded for the request to work. E.g in Javascript someone would use `encodeURIComponent()` like that 77 | ``` 78 | let filterObj = {movies: [1,2]}; 79 | fetch('/actors?filter=' + encodeURIComponent(JSON.stringify(filterObj))); 80 | ``` 81 | 82 | The above functionality is possible via this simple setup: 83 | ```java 84 | @RestController 85 | @RequestMapping("actors") 86 | public class ActorController { 87 | 88 | @Autowired 89 | private ActorRepository repository; 90 | 91 | @Autowired 92 | private FilterService filterService; 93 | 94 | @GetMapping 95 | public Iterable filterBy( 96 | @RequestParam(required = false, name = "filter") String filterStr, 97 | @RequestParam(required = false, name = "range") String rangeStr, 98 | @RequestParam(required = false, name="sort") String sortStr) { 99 | 100 | QueryParamWrapper wrapper = QueryParamExtractor.extract(filterStr, rangeStr, sortStr); 101 | return filterService.filterBy(wrapper, repository, Arrays.asList("firstName", "lastName")); 102 | } 103 | } 104 | ``` 105 | 106 | 107 | The main important parts include: 108 | 109 | - `@ControllerAdvice`s that wrap Collections in objects {content: []) with paging and number of results information along with Status Codes based on Exceptions thrown and returns 404 in case of null returned from endpoints. 110 | - `BaseRepository` interface that needs to be extended by each of resource `Repositories` 111 | - `CustomSpecifications` does all the magic of Criteria API query generation so that filtering and sorting works along with `FilterService` that provides some helper methods to the Controller code and helps provide convert the String query params to `FilterWrapper` so that it can be injected behind the scenes. 112 | - `ObjectMapperProvider` that can be used by the Spring Boot Application in case serialization and deserialization need to work through fields instead of Getters and Setters 113 | - you need to create classes annotated with `@ControllerAdvice` and extend the appropriate classes under package `springboot.rest.controllerAdvices` if needed in your project 114 | 115 | 116 | ## Installation 117 | 118 | For now installation is done through jitpack: 119 | 120 | Add this in your pom.xml repositories: 121 | 122 | 123 | 124 | jitpack.io 125 | https://jitpack.io 126 | 127 | ... 128 | 129 | 130 | and add this as a dependency in your pom.xml dependencies: 131 | 132 | 133 | com.github.zifnab87 134 | spring-boot-rest-api-helpers 135 | edb1770 136 | 137 | 138 | ## Usage 139 | 140 | - Add springboot.rest package in the scanBasePackages at the top of your Spring Boot Application class 141 | ```java 142 | @SpringBootApplication(scanBasePackages = {"com.myproject", springbootrest}); 143 | ``` 144 | - configure application.properties to use snake-case or camelCase for properties in API 145 | ``` 146 | spring-boot-rest-api-helpers.use-snake-case = false 147 | ``` 148 | - for each of the Rest API resources create a class `XYZ` that is annotated with `@Entity` 149 | - for each of the Rest API resources create an interface `XYZRepository` that extends `BaseRepository` 150 | - for each of the Rest API resources create a class `XYZController` annotated with `@RestController` 151 | - for each of Value object annotate them with with `com.nooul.apihelpers.springbootrest.annotations.ValueObject`. See `Sender` with `Mobile` and `MobileConverter` in test helpers. They should behave like plain strings. No comparisons are supported with Gte/Lte/Gt/Lt yet 152 | 153 | for more examples see/run the integration tests 154 | *Note:* three-level join tests are failing and are not implemented yet - Any help towards an implementation that allows any number of depth for queries would be greatly appreciated :D 155 | 156 | ## Previous Versions 157 | 158 | This repo used to be called `react-admin-java-rest` and it was used to provide the needed building blocks for building a real backend API like that can give responses to the above requests in conjunction with react-admin/admin-on-rest (used here together: https://github.com/zifnab87/admin-on-rest-demo-java-rest). Since the time of their first incarnation, it seemed obvious that those API helpers were useful outside of the react-admin REST API realm, so the name `spring-boot-rest-api-helpers` was given. 159 | 160 | 161 | ## Fully working example (outdated) 162 | 163 | For an example of how it can be used along admin-on-rest there is a fork of [admin-on-rest-demo](https://github.com/marmelab/admin-on-rest-demo) 164 | that is fully working and uses [react-admin-java-rest](https://github.com/zifnab87/react-admin-java-rest) 165 | 166 | Fully Working Fork of admin-on-rest-demo: [react-admin-demo-java-rest](https://github.com/zifnab87/react-admin-demo-java-rest) 167 | 168 | ## Release Notes 169 | - 0.9.0 - Support for Instant fields on Entities for date and date time range comparisons similar to Timestamp querying 170 | - 0.10.0 - Support for Value Objects that can be used in search with `q`, exact match and search by null -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.nooul.apihelpers 8 | spring-boot-rest-api-helpers 9 | 0.12.1.RELEASE 10 | jar 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 3.4.4 16 | 17 | 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-jpa 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-test 32 | 33 | 34 | com.vaadin.external.google 35 | android-json 36 | 37 | 38 | test 39 | 40 | 41 | org.projectlombok 42 | lombok 43 | 1.18.36 44 | 45 | 46 | com.h2database 47 | h2 48 | test 49 | 50 | 51 | org.json 52 | json 53 | 20250107 54 | 55 | 56 | org.apache.commons 57 | commons-text 58 | 1.13.0 59 | 60 | 61 | com.fasterxml.jackson.dataformat 62 | jackson-dataformat-csv 63 | 2.18.3 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-compiler-plugin 72 | 73 | UTF-8 74 | 1.8 75 | 1.8 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-jar-plugin 81 | 82 | 83 | 84 | true 85 | lib/ 86 | repository 87 | 88 | 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-shade-plugin 94 | 95 | 96 | package 97 | 98 | shade 99 | 100 | 101 | 102 | 103 | 104 | 105 | org.json:* 106 | 107 | 108 | 109 | 110 | org.json:* 111 | 112 | META-INF/*.MF 113 | 114 | 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-surefire-plugin 121 | 3.0.0-M5 122 | 123 | random 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/annotations/ValueObject.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.annotations; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.TYPE; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Target({ TYPE }) 10 | @Retention(RUNTIME) 11 | public @interface ValueObject { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/controllerAdvices/BodyAdvice.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.controllerAdvices; 2 | 3 | import lombok.NonNull; 4 | import lombok.Value; 5 | import org.springframework.core.MethodParameter; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.http.server.ServerHttpRequest; 9 | import org.springframework.http.server.ServerHttpResponse; 10 | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; 11 | 12 | import java.util.Arrays; 13 | 14 | //https://stackoverflow.com/a/40333275/986160 15 | //https://stackoverflow.com/a/59294075/986160 16 | //extend them and add @ControllerAdvice 17 | public class BodyAdvice implements ResponseBodyAdvice { 18 | @Override 19 | public boolean supports(MethodParameter returnType, Class converterType) { 20 | return true; 21 | } 22 | 23 | @Override 24 | @SuppressWarnings("unchecked") 25 | public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { 26 | 27 | // if (body == null) { 28 | // throw new NotFoundException("Resource was not found!"); 29 | // } 30 | if (isArray(body)) { 31 | return new Wrapper(Arrays.asList(body)); 32 | } 33 | if (body instanceof Iterable && !(body instanceof Page)) { 34 | return new Wrapper((Iterable)body); 35 | } 36 | return body; 37 | } 38 | 39 | @Value 40 | private class Wrapper { 41 | private final @NonNull Iterable content; 42 | 43 | } 44 | 45 | public static boolean isArray(Object obj) 46 | { 47 | return obj != null && (obj.getClass().isArray() || obj instanceof Iterable) && !(obj instanceof byte[]); 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/controllerAdvices/GlobalExceptionAdvice.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.controllerAdvices; 2 | 3 | //TODO add spring-security but make sure it is not auto-configured 4 | 5 | //import com.nooul.apihelpers.springbootrest.exceptions.NotFoundException; 6 | //import com.nooul.apihelpers.springbootrest.utils.JSON; 7 | //import lombok.AllArgsConstructor; 8 | //import lombok.Data; 9 | //import org.hibernate.TypeMismatchException; 10 | //import org.slf4j.Logger; 11 | //import org.slf4j.LoggerFactory; 12 | //import org.springframework.boot.context.properties.bind.BindException; 13 | //import org.springframework.http.HttpStatus; 14 | //import org.springframework.http.ResponseEntity; 15 | //import org.springframework.security.access.AccessDeniedException; 16 | //import org.springframework.security.authentication.BadCredentialsException; 17 | //import org.springframework.web.HttpMediaTypeNotSupportedException; 18 | //import org.springframework.web.bind.MethodArgumentNotValidException; 19 | //import org.springframework.web.bind.MissingServletRequestParameterException; 20 | //import org.springframework.web.bind.annotation.ExceptionHandler; 21 | //import org.springframework.web.context.request.ServletWebRequest; 22 | //import org.springframework.web.context.request.WebRequest; 23 | //import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 24 | //import org.springframework.web.multipart.support.MissingServletRequestPartException; 25 | //import org.springframework.web.server.MethodNotAllowedException; 26 | // 27 | //import javax.validation.ConstraintViolationException; 28 | //import java.util.Date; 29 | ////extend them and add @ControllerAdvice 30 | // 31 | //// based on https://www.baeldung.com/global-error-handler-in-a-spring-rest-api 32 | //public class GlobalExceptionAdvice { 33 | // 34 | // @Data 35 | // public class ErrorDetails { 36 | // private Date timestamp; 37 | // private String message; 38 | // private String details; 39 | // 40 | // public ErrorDetails(Date timestamp, String message, String details) { 41 | // super(); 42 | // this.timestamp = timestamp; 43 | // this.message = message; 44 | // this.details = details; 45 | // } 46 | // } 47 | // 48 | // @Data 49 | // @AllArgsConstructor 50 | // public class JsonLogMessage { 51 | // private String user; 52 | // private String uri; 53 | // private String exceptionMessage; 54 | // } 55 | // 56 | // private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionAdvice.class); 57 | // 58 | // @ExceptionHandler(Exception.class) 59 | // public final ResponseEntity unhandledExceptionHandler(Exception ex, WebRequest request) throws Exception { 60 | // logger.error("severe", prepareLogMessage(ex, request) + getFirstLinesOfStackTrace(ex, 5)); 61 | // throw ex; 62 | // } 63 | // 64 | // private String getFirstLinesOfStackTrace(Exception ex, int lineLimit) { 65 | // StackTraceElement[] stackTrace = ex.getStackTrace(); 66 | // String stackTraceStr = ""; 67 | // int linesLogged = 0; 68 | // if(stackTrace != null && stackTrace.length > 0) { 69 | // for (StackTraceElement e : stackTrace) { 70 | // if (linesLogged > lineLimit) { 71 | // break; 72 | // } 73 | // stackTraceStr += "\n" + e.toString(); 74 | // linesLogged++; 75 | // } 76 | // } 77 | // return stackTraceStr; 78 | // } 79 | // 80 | // @ExceptionHandler(BadCredentialsException.class) 81 | // public final ResponseEntity UnauthorizedHandler401(Exception ex, WebRequest request) { 82 | // ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), 83 | // request.getDescription(false)); 84 | // 85 | // logger.warn("warning", prepareLogMessage(ex, request)); 86 | // 87 | // return new ResponseEntity<>(errorDetails, HttpStatus.UNAUTHORIZED); 88 | // } 89 | // 90 | // @ExceptionHandler(AccessDeniedException.class) 91 | // public final ResponseEntity ForbiddenHandler403(Exception ex, WebRequest request) { 92 | // ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), 93 | // request.getDescription(false)); 94 | // 95 | // logger.warn("warning", prepareLogMessage(ex, request)); 96 | // 97 | // return new ResponseEntity<>(errorDetails, HttpStatus.FORBIDDEN); 98 | // } 99 | // 100 | // @ExceptionHandler(value = {IllegalArgumentException.class, 101 | // MissingServletRequestPartException.class, 102 | // MissingServletRequestParameterException.class, 103 | // MethodArgumentTypeMismatchException.class, 104 | // TypeMismatchException.class, 105 | // ConstraintViolationException.class, 106 | // BindException.class, 107 | // MethodArgumentNotValidException.class}) 108 | // public final ResponseEntity BadRequestHandler400(Exception ex, WebRequest request) { 109 | // ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), 110 | // request.getDescription(false)); 111 | // 112 | // logger.warn("warning", prepareLogMessage(ex, request)); 113 | // 114 | // return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST); 115 | // } 116 | // 117 | // @ExceptionHandler(value = {MethodNotAllowedException.class}) 118 | // public final ResponseEntity MethodNotAllowedHandler405(Exception ex, WebRequest request) { 119 | // ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), 120 | // request.getDescription(false)); 121 | // 122 | // logger.warn("warning", prepareLogMessage(ex, request)); 123 | // 124 | // return new ResponseEntity<>(errorDetails, HttpStatus.METHOD_NOT_ALLOWED); 125 | // } 126 | // 127 | // @ExceptionHandler(value = {HttpMediaTypeNotSupportedException.class}) 128 | // public final ResponseEntity UnsupportedMediaTypeHandler415(Exception ex, WebRequest request) { 129 | // ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), 130 | // request.getDescription(false)); 131 | // 132 | // logger.warn("warning", prepareLogMessage(ex, request)); 133 | // 134 | // return new ResponseEntity<>(errorDetails, HttpStatus.UNSUPPORTED_MEDIA_TYPE); 135 | // } 136 | // 137 | // 138 | // 139 | // @ExceptionHandler(NotFoundException.class) 140 | // public final ResponseEntity notFoundHandler(Exception ex, WebRequest request) { 141 | // ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), 142 | // request.getDescription(false)); 143 | // 144 | // logger.warn("warning", prepareLogMessage(ex, request)); 145 | // 146 | // return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND); 147 | // } 148 | // 149 | // private String prepareLogMessage(Exception ex, WebRequest request) { 150 | // String uri = ""; 151 | // String user = ""; 152 | // if (request instanceof ServletWebRequest) { 153 | // ServletWebRequest servletWebRequest = (ServletWebRequest) request; 154 | // uri = servletWebRequest.getRequest().getRequestURI(); 155 | // user = servletWebRequest.getRemoteUser(); 156 | // } 157 | // JsonLogMessage jsonObj = new JsonLogMessage(user, uri, "[" + ex.getClass().getName() + ": " + ex.getMessage() + "]"); 158 | // return JSON.toJsonString(jsonObj); 159 | // } 160 | //} -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/controllerAdvices/ResourceSizeAdvice.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.controllerAdvices; 2 | 3 | import org.springframework.core.MethodParameter; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.http.converter.HttpMessageConverter; 7 | import org.springframework.http.server.ServerHttpRequest; 8 | import org.springframework.http.server.ServerHttpResponse; 9 | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; 10 | //extend them and add @ControllerAdvice 11 | public class ResourceSizeAdvice implements ResponseBodyAdvice> { 12 | 13 | @Override 14 | public boolean supports(MethodParameter returnType, Class> converterType) { 15 | //Checks if this advice is applicable. 16 | //In this case it applies to any endpoint which returns a page. 17 | return Page.class.isAssignableFrom(returnType.getParameterType()); 18 | } 19 | 20 | @Override 21 | public Page beforeBodyWrite(Page page, MethodParameter methodParameter, MediaType mediaType, Class> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { 22 | serverHttpResponse.getHeaders().add("X-Total-Count",String.valueOf(page.getTotalElements())); 23 | return page; 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/entities/ErrorDetails.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.entities; 2 | 3 | import java.util.Date; 4 | 5 | public class ErrorDetails { 6 | private Date timestamp; 7 | private String message; 8 | private String details; 9 | 10 | public ErrorDetails(Date timestamp, String message, String details) { 11 | super(); 12 | this.timestamp = timestamp; 13 | this.message = message; 14 | this.details = details; 15 | } 16 | 17 | public Date getTimestamp() { 18 | return timestamp; 19 | } 20 | 21 | public String getMessage() { 22 | return message; 23 | } 24 | 25 | public String getDetails() { 26 | return details; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/entities/QueryParamWrapper.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.entities; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.RequiredArgsConstructor; 6 | import org.json.JSONArray; 7 | import org.json.JSONObject; 8 | 9 | @Getter 10 | @RequiredArgsConstructor 11 | @NoArgsConstructor(force = true) 12 | public class QueryParamWrapper { 13 | private final JSONObject filter; 14 | private final JSONArray filterOr; 15 | private final JSONArray range; 16 | private final JSONArray sort; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/exceptions/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.NOT_FOUND,reason = "resource not found") 7 | public class NotFoundException extends RuntimeException { 8 | public NotFoundException(String msg) { 9 | super(msg); 10 | } 11 | public NotFoundException() {} 12 | 13 | @Override 14 | public synchronized Throwable fillInStackTrace() { 15 | return this; 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/providers/ObjectMapperProvider.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.providers; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.PropertyAccessor; 5 | import com.fasterxml.jackson.databind.*; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | public class ObjectMapperProvider { 12 | 13 | @Autowired 14 | private Environment env; 15 | 16 | public ObjectMapper getObjectMapper() { 17 | ObjectMapper mapper = new ObjectMapper(); 18 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 19 | mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true); 20 | mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); 21 | 22 | mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); 23 | mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); 24 | String usesSnakeCase = env.getProperty("spring-boot-rest-api-helpers.use-snake-case"); 25 | if (usesSnakeCase != null && usesSnakeCase.equals("true")) { 26 | mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); 27 | } 28 | return mapper; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/repositories/BaseRepository.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.repositories; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 5 | import org.springframework.data.repository.NoRepositoryBean; 6 | 7 | import java.io.Serializable; 8 | 9 | @NoRepositoryBean 10 | public interface BaseRepository extends JpaRepository, JpaSpecificationExecutor { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/serializers/IdWrapperSerializer.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.serializers; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerationException; 4 | import com.fasterxml.jackson.core.JsonGenerator; 5 | import com.fasterxml.jackson.databind.SerializerProvider; 6 | import com.fasterxml.jackson.databind.ser.std.StdSerializer; 7 | 8 | import java.io.IOException; 9 | 10 | public class IdWrapperSerializer extends StdSerializer { 11 | 12 | public IdWrapperSerializer() { 13 | super(Integer.class); 14 | } 15 | 16 | public IdWrapperSerializer(Class t) { 17 | super(t); 18 | } 19 | 20 | @Override 21 | public void serialize(Integer swe, 22 | JsonGenerator jgen, 23 | SerializerProvider sp) throws IOException, JsonGenerationException { 24 | 25 | jgen.writeStartObject(); 26 | jgen.writeNumberField("id", swe); 27 | jgen.writeEndObject(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/services/FilterService.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.services; 2 | 3 | 4 | import com.nooul.apihelpers.springbootrest.entities.QueryParamWrapper; 5 | import com.nooul.apihelpers.springbootrest.repositories.BaseRepository; 6 | import com.nooul.apihelpers.springbootrest.specifications.CustomSpecifications; 7 | import org.apache.commons.text.CaseUtils; 8 | import org.json.JSONArray; 9 | import org.json.JSONObject; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.core.env.Environment; 12 | import org.springframework.data.domain.Page; 13 | import org.springframework.data.domain.PageRequest; 14 | import org.springframework.data.domain.Sort; 15 | import org.springframework.data.jpa.domain.Specification; 16 | import org.springframework.stereotype.Service; 17 | 18 | import java.io.Serializable; 19 | import java.util.ArrayList; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Set; 23 | 24 | @Service 25 | //from: https://github.com/Nooul/spring-boot-rest-api-helpers/blob/master/src/main/java/com/nooul/apihelpers/springbootrest/services/FilterService.java 26 | public class FilterService { 27 | 28 | @Autowired 29 | private Environment env; 30 | 31 | @Autowired 32 | private CustomSpecifications specifications; 33 | 34 | 35 | public long countBy(QueryParamWrapper queryParamWrapper, BaseRepository repo) { 36 | JSONObject filter = queryParamWrapper.getFilter(); 37 | JSONArray filterOr = queryParamWrapper.getFilterOr(); 38 | String usesSnakeCase = env.getProperty("spring-boot-rest-api-helpers.use-snake-case"); 39 | if (filter != null && filter.length() > 0) { 40 | HashMap map = (HashMap) filter.toMap(); 41 | 42 | if (usesSnakeCase != null && usesSnakeCase.equals("true")) { 43 | map = convertToCamelCase(map); 44 | } 45 | 46 | return repo.count( 47 | specifications.build(map)); 48 | 49 | } else if (filterOr != null && filterOr.length() > 0) { 50 | List list = filterOr.toList(); 51 | if (usesSnakeCase != null && usesSnakeCase.equals("true")) { 52 | //map = convertToCamelCase(map); TODO for list 53 | } 54 | return repo.count(specifications.build(list)); 55 | 56 | } else { 57 | return repo.count(); 58 | } 59 | } 60 | 61 | public Page filterBy(QueryParamWrapper queryParamWrapper, BaseRepository repo) { 62 | return filterByHelper(repo, specifications, queryParamWrapper, "id", new ArrayList<>()); 63 | } 64 | 65 | public Page filterBy(QueryParamWrapper queryParamWrapper, BaseRepository repo, String primaryKeyName) { 66 | 67 | return filterByHelper(repo, specifications, queryParamWrapper, primaryKeyName, new ArrayList<>()); 68 | } 69 | 70 | public Page filterBy(QueryParamWrapper queryParamWrapper, BaseRepository repo, String primaryKeyName, List searchOnlyInFields) { 71 | 72 | return filterByHelper(repo, specifications, queryParamWrapper, primaryKeyName, searchOnlyInFields); 73 | } 74 | 75 | public Page filterBy(QueryParamWrapper queryParamWrapper, BaseRepository repo, List searchOnlyInFields) { 76 | 77 | return filterByHelper(repo, specifications, queryParamWrapper, "id", searchOnlyInFields); 78 | } 79 | 80 | private List sortHelper(JSONArray sort, String primaryKeyName) { 81 | 82 | List sortOrders = new ArrayList<>(); 83 | String usesSnakeCase = env.getProperty("spring-boot-rest-api-helpers.use-snake-case"); 84 | if (sort.length() % 2 != 0) { 85 | throw new IllegalArgumentException("sort should have even length given as array e.g ['name', 'ASC', 'birthDate', 'DESC']"); 86 | } 87 | for (int i = 0; i < sort.length(); i = i + 2) { 88 | String sortBy; 89 | if (usesSnakeCase != null && usesSnakeCase.equals("true")) { 90 | sortBy = convertToCamelCase((String) sort.get(i)); 91 | } else { 92 | sortBy = (String) sort.get(i); 93 | } 94 | 95 | sortOrders.add(new Sort.Order(Sort.Direction.valueOf((String) sort.get(i + 1)), sortBy)); 96 | } 97 | if (sortOrders.isEmpty()) { 98 | sortOrders.add(new Sort.Order(Sort.Direction.ASC, primaryKeyName)); 99 | } 100 | 101 | return sortOrders; 102 | } 103 | 104 | private Page filterByHelper(BaseRepository repo, 105 | CustomSpecifications specifications, 106 | QueryParamWrapper queryParamWrapper, 107 | String primaryKeyName, 108 | List searchOnlyInFields) { 109 | String usesSnakeCase = env.getProperty("spring-boot-rest-api-helpers.use-snake-case"); 110 | 111 | Sort sortObj; 112 | JSONObject filter = queryParamWrapper.getFilter(); 113 | JSONArray filterOr = queryParamWrapper.getFilterOr(); 114 | JSONArray range = queryParamWrapper.getRange(); 115 | JSONArray sort = queryParamWrapper.getSort(); 116 | 117 | int page = 0; 118 | int size = Integer.MAX_VALUE; 119 | if (range.length() == 2) { 120 | page = (Integer) range.get(0); 121 | size = (Integer) range.get(1); 122 | } 123 | 124 | sortObj = Sort.by(sortHelper(sort, primaryKeyName)); 125 | Page result; 126 | if (filter != null && filter.length() > 0) { 127 | HashMap map = (HashMap) filter.toMap(); 128 | 129 | if (usesSnakeCase != null && usesSnakeCase.equals("true")) { 130 | map = convertToCamelCase(map); 131 | } 132 | 133 | 134 | result = repo.findAll(specifications.build(map, searchOnlyInFields), PageRequest.of(page, size, sortObj)); 135 | 136 | } else if (filterOr != null && filterOr.length() > 0) { 137 | 138 | List list = filterOr.toList(); 139 | if (usesSnakeCase != null && usesSnakeCase.equals("true")) { 140 | //map = convertToCamelCase(map); TODO for list 141 | } 142 | 143 | result = repo.findAll( 144 | specifications.build(list) 145 | , PageRequest.of(page, size, sortObj)); 146 | 147 | } else { 148 | result = repo.findAll(PageRequest.of(page, size, sortObj)); 149 | } 150 | return result; 151 | } 152 | 153 | private HashMap convertToCamelCase(HashMap snakeCaseMap) { 154 | HashMap camelCaseMap = new HashMap<>(); 155 | for (String key : snakeCaseMap.keySet()) { 156 | Object val = snakeCaseMap.get(key); 157 | camelCaseMap.put(convertToCamelCase(key), val); 158 | } 159 | return camelCaseMap; 160 | } 161 | 162 | public String convertToCamelCase(String snakeCaseStr) { 163 | return CaseUtils.toCamelCase(snakeCaseStr,false, new char[]{'_'}); 164 | } 165 | } -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/specifications/CustomSpecifications.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.specifications; 2 | 3 | 4 | import com.nooul.apihelpers.springbootrest.annotations.ValueObject; 5 | import jakarta.persistence.EntityManager; 6 | import jakarta.persistence.PersistenceContext; 7 | import jakarta.persistence.criteria.*; 8 | import jakarta.persistence.metamodel.Attribute; 9 | import jakarta.persistence.metamodel.IdentifiableType; 10 | import jakarta.persistence.metamodel.Metamodel; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.springframework.data.jpa.domain.Specification; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.sql.Timestamp; 16 | import java.text.DateFormat; 17 | import java.text.ParseException; 18 | import java.text.SimpleDateFormat; 19 | import java.util.*; 20 | 21 | //from: https://github.com/Nooul/spring-boot-rest-api-helpers/blob/master/src/main/java/com/nooul/apihelpers/springbootrest/specifications/CustomSpecifications.java 22 | @Service 23 | public class CustomSpecifications { 24 | 25 | @PersistenceContext 26 | private EntityManager em; 27 | 28 | public Specification build(Map map) { 29 | return (root, query, builder) -> { 30 | query.distinct(true); 31 | List predicates = handleMap(builder, root, null, query, map, new ArrayList<>(), false); 32 | return builder.and(predicates.toArray(new Predicate[predicates.size()])); 33 | }; 34 | } 35 | 36 | 37 | public Specification build(Map map, List includeOnlyFields) { 38 | return (root, query, builder) -> { 39 | query.distinct(true); 40 | if (map.containsKey("allowDuplicates") && map.get("allowDuplicates") instanceof Boolean && (boolean) map.get("allowDuplicates")) { 41 | query.distinct(false); 42 | } 43 | map.remove("allowDuplicates"); 44 | List predicates = handleMap(builder, root, null, query, map, includeOnlyFields,false); 45 | return builder.and(predicates.toArray(new Predicate[predicates.size()])); 46 | }; 47 | } 48 | 49 | 50 | public Specification build(List> list) { 51 | return (root, query, builder) -> { 52 | 53 | query.distinct(true); 54 | List orPredicates = new ArrayList<>(); 55 | boolean needsLeftJoin = false; 56 | if (containsMapsWithNullValues(list)) { 57 | needsLeftJoin = true; 58 | } 59 | 60 | for (Map map : list) { 61 | List predicates = handleMap(builder, root, null, query, map, new ArrayList<>(), needsLeftJoin); 62 | Predicate orPred = builder.and(predicates.toArray(new Predicate[predicates.size()])); 63 | orPredicates.add(orPred); 64 | } 65 | return builder.or(orPredicates.toArray(new Predicate[orPredicates.size()])); 66 | 67 | }; 68 | } 69 | 70 | 71 | private boolean containsMapsWithNullValues(Collection values) { 72 | for (Object obj : values) { 73 | if (obj instanceof Map) { 74 | Map map = (Map) obj; 75 | boolean containsNull = map.values().stream().anyMatch(v -> v == null); 76 | if (containsNull) { 77 | return true; 78 | } 79 | } 80 | } 81 | return false; 82 | } 83 | 84 | 85 | 86 | 87 | public List handleMap(CriteriaBuilder builder, Root root, Join join, CriteriaQuery query, Map map, List includeOnlyFields, boolean needsLeftJoin) { 88 | if (join != null){ 89 | root = query.from(getJavaTypeOfClassContainingAttribute(root, join.getAttribute().getName(), needsLeftJoin)); 90 | } 91 | 92 | List predicates = new ArrayList<>(); 93 | Predicate pred; 94 | if (map.containsKey("q")) { 95 | if (map.get("q") instanceof String || map.get("q") instanceof Number) { 96 | predicates.add(searchInAllAttributesPredicate(builder, root, String.valueOf(map.get("q")), includeOnlyFields)); 97 | } 98 | map.remove("q"); 99 | } 100 | Set> attributes = root.getModel().getAttributes(); 101 | for (Map.Entry e : map.entrySet()) { 102 | String key = (String) e.getKey(); 103 | Object val = e.getValue(); 104 | String cleanKey = cleanUpKey(key); 105 | 106 | Attribute a = root.getModel().getAttribute(cleanKey); 107 | if (attributes.contains(a)) { 108 | pred = handleAllCases(builder, root, join, query, a, key, val, needsLeftJoin); 109 | predicates.add(pred); 110 | } 111 | } 112 | return predicates; 113 | } 114 | 115 | public Predicate handleAllCases(CriteriaBuilder builder, Root root, Join join, CriteriaQuery query, Attribute a, String key, Object val, boolean needsLeftJoin) { 116 | boolean isValueCollection = val instanceof Collection; 117 | boolean isValueMap = val instanceof Map; 118 | String cleanKey = cleanUpKey(key); 119 | boolean isKeyClean = cleanKey.equals(key); 120 | boolean isNegation = key.endsWith("Not"); 121 | boolean isGt = key.endsWith("Gt"); 122 | boolean isGte = key.endsWith("Gte"); 123 | boolean isLt = key.endsWith("Lt"); 124 | boolean isLte = key.endsWith("Lte"); 125 | boolean isConjunction = key.endsWith("And"); 126 | boolean isAssociation = a.isAssociation(); 127 | 128 | if (val instanceof Collection && containsNull((Collection) val)) { 129 | needsLeftJoin = true; 130 | } 131 | 132 | 133 | if (isValueMap) { 134 | val = convertMapContainingPrimaryIdToValue(val, a, root, needsLeftJoin); 135 | } 136 | if (val instanceof Map && isAssociation) { 137 | List predicates = handleMap(builder, root, addJoinIfNotExists(root,a, isValueCollection, isConjunction, needsLeftJoin), query, ((Map)val), Arrays.asList(), needsLeftJoin); 138 | Predicate[] predicatesArray = predicates.toArray(new Predicate[predicates.size()]); 139 | return builder.and(predicatesArray); 140 | } 141 | 142 | 143 | 144 | if (isKeyClean) { 145 | return handleCleanKeyCase(builder, root, join, query, cleanKey, a, val, needsLeftJoin); 146 | } else if (isNegation) { 147 | return builder.not(handleCleanKeyCase(builder, root, join, query, cleanKey, a, val, needsLeftJoin)); 148 | } else if (isConjunction) { 149 | if (isValueCollection) { 150 | return handleCollection(builder, root, join, query, a, cleanKey, (Collection) val, true, needsLeftJoin); 151 | } 152 | } else if (isLte) { 153 | return createLtePredicate(builder, root, a, val); 154 | } else if (isGte) { 155 | return createGtePredicate(builder, root, a, val); 156 | } else if (isLt) { 157 | return createLtPredicate(builder, root, a, val); 158 | } else if (isGt) { 159 | return createGtPredicate(builder, root, a, val); 160 | } 161 | return builder.conjunction(); 162 | } 163 | 164 | private boolean containsNull(Collection val) { 165 | for (Object ent : val) { 166 | if (ent == null) { 167 | return true; 168 | } 169 | } 170 | return false; 171 | } 172 | 173 | public Predicate handleCollection(CriteriaBuilder builder, Root root, Join join, CriteriaQuery query, Attribute a, String key, Collection values, boolean conjunction, boolean needsLeftJoin) { 174 | 175 | List predicates = new ArrayList<>(); 176 | 177 | 178 | 179 | for (Object val : values) { 180 | Predicate pred = handleAllCases(builder, root, join, query, a, key, val, needsLeftJoin); 181 | predicates.add(pred); 182 | } 183 | Predicate[] predicatesArray = predicates.toArray(new Predicate[predicates.size()]); 184 | return (conjunction) ? builder.and(predicatesArray): builder.or(predicatesArray); 185 | } 186 | 187 | 188 | public Predicate handleCleanKeyCase(CriteriaBuilder builder, Root root, Join join, CriteriaQuery query, String key, Attribute a, Object val, boolean needsLeftJoin) { 189 | boolean isValueCollection = val instanceof Collection; 190 | boolean isValTextSearch = (val instanceof String) && ((String) val).contains("%"); 191 | if (isValueCollection && !a.isAssociation()) { 192 | return handleCollection(builder, root, join, query, a, key, (Collection) val, false, needsLeftJoin); 193 | } else if(isValueCollection && a.isAssociation()) { 194 | return handleCollection(builder, root, join, query, a, key, (Collection) val, false, needsLeftJoin); 195 | } else if (isValTextSearch) { 196 | return createLikePredicate(builder, root, join, a, (String) val); 197 | } else if(a.isCollection() && !a.isAssociation()) { 198 | return createEqualityPredicate(builder, root, addJoinIfNotExists(root, a, false, isValueCollection, needsLeftJoin), a, val, needsLeftJoin); 199 | } else { 200 | return createEqualityPredicate(builder, root, join, a, val, needsLeftJoin); 201 | } 202 | } 203 | 204 | 205 | //https://stackoverflow.com/a/16911313/986160 206 | //https://stackoverflow.com/a/47793003/986160 207 | public Attribute getIdAttribute(EntityManager em, Class clazz) { 208 | Metamodel m = em.getMetamodel(); 209 | IdentifiableType of = (IdentifiableType) m.managedType(clazz); 210 | return of.getId(of.getIdType().getJavaType()); 211 | } 212 | 213 | private String cleanUpKey(String key) { 214 | 215 | List postfixes = Arrays.asList("Gte", "Gt", "Lte", "Lt", "Not", "And"); 216 | for (String postfix : postfixes) { 217 | if (key.endsWith(postfix)) { 218 | return key.substring(0, key.length() - postfix.length()); 219 | } 220 | } 221 | return key; 222 | } 223 | 224 | public Predicate searchInAllAttributesPredicate(CriteriaBuilder builder, Root root, String text, List includeOnlyFields) { 225 | 226 | if (!text.contains("%")) { 227 | text = "%" + text + "%"; 228 | } 229 | final String finalText = text; 230 | 231 | Set attributes = root.getModel().getAttributes(); 232 | List orPredicates = new ArrayList<>(); 233 | for (Attribute a : attributes) { 234 | boolean javaTypeIsString = a.getJavaType().getSimpleName().equalsIgnoreCase("string"); 235 | boolean shouldSearch = includeOnlyFields.isEmpty() || includeOnlyFields.contains(a.getName()); 236 | if ((javaTypeIsString || isUUID(a) || isValueObject(a)) && shouldSearch) { 237 | Predicate orPred = builder.like(root.get(a.getName()).as(String.class), finalText); 238 | orPredicates.add(orPred); 239 | } 240 | 241 | } 242 | 243 | return builder.or(orPredicates.toArray(new Predicate[orPredicates.size()])); 244 | 245 | } 246 | 247 | 248 | private Predicate createEqualityPredicate(CriteriaBuilder builder, Root root, Join join, Attribute a, Object val, boolean needsLeftJoin) { 249 | if (isNull(a, val)) { 250 | if (a.isAssociation() && a.isCollection()) { 251 | return builder.isEmpty(root.get(a.getName())); 252 | } else if(isPrimitive(a)) { 253 | return builder.isNull(root.get(a.getName())); 254 | } else if (isValueObject(a)) { 255 | return builder.equal(root.get(a.getName()).as(String.class), "null"); 256 | }else { 257 | return root.get(a.getName()).isNull(); 258 | } 259 | } 260 | else if (join == null) { 261 | if (isEnum(a)) { 262 | return builder.equal(root.get(a.getName()), Enum.valueOf(Class.class.cast(a.getJavaType()), (String) val)); 263 | } else if (isPrimitive(a)) { 264 | return builder.equal(root.get(a.getName()), val); 265 | } else if (isValueObject(a)) { 266 | return builder.equal(root.get(a.getName()).as(String.class), val); 267 | } else if(isUUID(a)) { 268 | return builder.equal(root.get(a.getName()), UUID.fromString(val.toString())); 269 | } else if(a.isAssociation()) { 270 | if (isPrimaryKeyOfAttributeUUID(a, root, needsLeftJoin)) { 271 | return prepareJoinAssociatedPredicate(builder, root, a, UUID.fromString(val.toString()), needsLeftJoin); 272 | } 273 | else { 274 | return prepareJoinAssociatedPredicate(builder, root, a, val, needsLeftJoin); 275 | } 276 | } 277 | } 278 | else if (join != null) { 279 | if (isEnum(a)) { 280 | return builder.equal(join.get(a.getName()), Enum.valueOf(Class.class.cast(a.getJavaType()), (String) val)); 281 | } else if (isPrimitive(a)) { 282 | return builder.equal(join.get(a.getName()), val); 283 | } else if (isValueObject(a)) { 284 | return builder.equal(join.get(a.getName()).as(String.class), val); 285 | } else if (a.isAssociation()) { 286 | Path associationPath = join.get(a.getName()); 287 | String associationPathIdKeyName = getIdAttribute(em, associationPath.getJavaType()).getName(); 288 | return builder.equal(associationPath.get(associationPathIdKeyName), val); 289 | } else if(a.isCollection()) { 290 | return builder.equal(join, val); 291 | } 292 | } 293 | throw new IllegalArgumentException("equality/inequality is currently supported on primitives, enums and value objects"); 294 | } 295 | 296 | private Predicate createLikePredicate(CriteriaBuilder builder, Root root, Join join, Attribute a, String val) { 297 | if (join == null) { 298 | return builder.like(root.get(a.getName()).as(String.class), val); 299 | } 300 | else { 301 | return builder.like(join.get(a.getName()).as(String.class), val); 302 | } 303 | } 304 | 305 | private Predicate createGtPredicate(CriteriaBuilder builder, Root root, Attribute a, Object val) { 306 | if (val instanceof String) { 307 | Timestamp timestamp = timeStamp((String)val); 308 | if (timestamp != null && !isInstant(a)) { 309 | return builder.greaterThan(root.get(a.getName()), timestamp); 310 | } else if (timestamp != null && isInstant(a)) { 311 | return builder.greaterThan(root.get(a.getName()), timestamp.toInstant()); 312 | } 313 | 314 | return builder.greaterThan(builder.lower(root.get(a.getName())), ((String) val).toLowerCase()); 315 | } else if (val instanceof Integer) { 316 | return builder.greaterThan(root.get(a.getName()), (Integer) val); 317 | } 318 | throw new IllegalArgumentException("val type not supported yet"); 319 | } 320 | 321 | 322 | private Predicate createGtePredicate(CriteriaBuilder builder, Root root, Attribute a, Object val) { 323 | if (val instanceof String) { 324 | Timestamp timestamp = timeStamp((String)val); 325 | if (timestamp != null && !isInstant(a)) { 326 | return builder.greaterThanOrEqualTo(root.get(a.getName()), timestamp); 327 | } else if(timestamp != null && isInstant(a)) { 328 | return builder.greaterThanOrEqualTo(root.get(a.getName()), timestamp.toInstant()); 329 | } 330 | return builder.greaterThanOrEqualTo(builder.lower(root.get(a.getName())), ((String) val).toLowerCase()); 331 | } else if (val instanceof Integer) { 332 | return builder.greaterThanOrEqualTo(root.get(a.getName()), (Integer) val); 333 | } 334 | throw new IllegalArgumentException("val type not supported yet"); 335 | } 336 | 337 | private Predicate createLtPredicate(CriteriaBuilder builder, Root root, Attribute a, Object val) { 338 | if (val instanceof String) { 339 | Timestamp timestamp = timeStamp((String)val); 340 | if (timestamp != null && !isInstant(a)) { 341 | return builder.lessThan(root.get(a.getName()), timestamp); 342 | } else if(timestamp != null && isInstant(a)) { 343 | return builder.lessThan(root.get(a.getName()), timestamp.toInstant()); 344 | } 345 | return builder.lessThan(builder.lower(root.get(a.getName())), ((String) val).toLowerCase()); 346 | } else if (val instanceof Integer) { 347 | return builder.lessThan(root.get(a.getName()), (Integer) val); 348 | } 349 | throw new IllegalArgumentException("val type not supported yet"); 350 | } 351 | 352 | private Predicate createLtePredicate(CriteriaBuilder builder, Root root, Attribute a, Object val) { 353 | if (val instanceof String) { 354 | Timestamp timestamp = timeStamp((String)val); 355 | if (timestamp != null && !isInstant(a)) { 356 | return builder.lessThanOrEqualTo(root.get(a.getName()), timestamp); 357 | } else if (timestamp != null && isInstant(a)) { 358 | return builder.lessThanOrEqualTo(root.get(a.getName()), timestamp.toInstant()); 359 | } 360 | return builder.lessThanOrEqualTo(builder.lower(root.get(a.getName())), ((String) val).toLowerCase()); 361 | } else if (val instanceof Integer) { 362 | return builder.lessThanOrEqualTo(root.get(a.getName()), (Integer) val); 363 | } 364 | throw new IllegalArgumentException("val type not supported yet"); 365 | } 366 | 367 | private static Timestamp timeStamp(String dateStr) { 368 | DateFormat dateFormat; 369 | if (dateStr.contains("T")) { 370 | dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 371 | } 372 | else { 373 | dateFormat = new SimpleDateFormat("yyyy-MM-dd"); 374 | } 375 | Date date; 376 | try { 377 | date = dateFormat.parse(dateStr); 378 | } catch (ParseException e) { 379 | return null; 380 | } 381 | long time = date.getTime(); 382 | return new Timestamp(time); 383 | } 384 | 385 | 386 | private Predicate prepareJoinAssociatedPredicate(CriteriaBuilder builder, Root root, Attribute a, Object val, boolean needsLeftJoin) { 387 | 388 | 389 | Path rootJoinGetName = addJoinIfNotExists(root, a, false, false, needsLeftJoin); 390 | Class referencedClass = rootJoinGetName.getJavaType(); 391 | String referencedPrimaryKey = getIdAttribute(em, referencedClass).getName(); 392 | return builder.equal(rootJoinGetName.get(referencedPrimaryKey), val); 393 | } 394 | 395 | 396 | private Join addJoinIfNotExists(Root root, Attribute a, boolean isConjunction, boolean isValueCollection, boolean needsLeftJoin) { 397 | if(isConjunction && isValueCollection) { 398 | return root.join(a.getName(), needsLeftJoin ? JoinType.LEFT : JoinType.INNER); 399 | } 400 | 401 | Set joins = root.getJoins(); 402 | Join toReturn = null; 403 | for (Join join: joins) { 404 | if (a.getName().equals(join.getAttribute().getName())){ 405 | toReturn = join; 406 | break; 407 | } 408 | } 409 | if (toReturn == null) { 410 | toReturn = root.join(a.getName(), needsLeftJoin ? JoinType.LEFT : JoinType.INNER); 411 | } 412 | return toReturn; 413 | } 414 | 415 | 416 | private Class getJavaTypeOfClassContainingAttribute(Root root, String attributeName, boolean needsLeftJoin) { 417 | Attribute a = root.getModel().getAttribute(attributeName); 418 | if (a.isAssociation()) { 419 | return addJoinIfNotExists(root, a, false, false, needsLeftJoin).getJavaType(); 420 | } 421 | return null; 422 | } 423 | 424 | private boolean isPrimaryKeyOfAttributeUUID(Attribute a, Root root, boolean needsLeftJoin) { 425 | Class javaTypeOfAttribute = getJavaTypeOfClassContainingAttribute(root, a.getName(), needsLeftJoin); 426 | String primaryKeyName = getIdAttribute(em, javaTypeOfAttribute).getJavaType().getSimpleName().toLowerCase(); 427 | return primaryKeyName.equalsIgnoreCase("uuid"); 428 | } 429 | 430 | private Object convertMapContainingPrimaryIdToValue(Object val, Attribute a, Root root, boolean needsLeftJoin) { 431 | Class javaTypeOfAttribute = getJavaTypeOfClassContainingAttribute(root, a.getName(), needsLeftJoin); 432 | String primaryKeyName = getIdAttribute(em, javaTypeOfAttribute).getName(); 433 | if (val instanceof Map && ((Map) val).keySet().size() == 1) { 434 | Map map = ((Map) val); 435 | for (Object key: map.keySet()) { 436 | if (key.equals(primaryKeyName)) { 437 | return map.get(primaryKeyName); 438 | } 439 | } 440 | } 441 | return val; 442 | } 443 | 444 | private boolean isValueObject(Attribute attribute) { 445 | boolean test = attribute.getJavaType().isAnnotationPresent(ValueObject.class); 446 | return test; 447 | } 448 | 449 | private boolean isUUID(Attribute attribute) { 450 | String attributeJavaClass = attribute.getJavaType().getSimpleName().toLowerCase(); 451 | return attributeJavaClass.equalsIgnoreCase("uuid"); 452 | } 453 | 454 | private boolean isInstant(Attribute attribute) { 455 | String attributeJavaClass = attribute.getJavaType().getSimpleName().toLowerCase(); 456 | return attributeJavaClass.equals("instant"); 457 | } 458 | 459 | private boolean isPrimitive(Attribute attribute) { 460 | String attributeJavaClass = attribute.getJavaType().getSimpleName().toLowerCase(); 461 | return attributeJavaClass.startsWith("int") || 462 | attributeJavaClass.startsWith("long") || 463 | attributeJavaClass.equals("boolean") || 464 | attributeJavaClass.equals("string") || 465 | attributeJavaClass.equals("float") || 466 | attributeJavaClass.equals("double"); 467 | } 468 | 469 | private boolean isPrimitiveValue(Object obj) { 470 | String javaClass = obj.getClass().getSimpleName().toLowerCase(); 471 | return javaClass.startsWith("int") || 472 | javaClass.startsWith("long") || 473 | javaClass.equals("boolean") || 474 | javaClass.equals("string") || 475 | javaClass.equals("float") || 476 | javaClass.equals("double"); 477 | } 478 | 479 | private boolean isEnum(Attribute attribute) { 480 | String parentJavaClass = ""; 481 | if (attribute.getJavaType().getSuperclass() != null) { 482 | parentJavaClass = attribute.getJavaType().getSuperclass().getSimpleName().toLowerCase(); 483 | } 484 | return parentJavaClass.equals("enum"); 485 | } 486 | 487 | private boolean isNull(Attribute attribute, Object val) { 488 | if (isPrimitive(attribute)) { 489 | String attributeJavaClass = attribute.getJavaType().getSimpleName().toLowerCase(); 490 | if (attributeJavaClass.equals("string")) { 491 | String valObj = (String) val; 492 | return StringUtils.isBlank(valObj) || valObj.equalsIgnoreCase("null"); 493 | } 494 | else { 495 | return val == null; 496 | } 497 | } 498 | else { 499 | return val == null; 500 | } 501 | } 502 | } -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/specifications/CustomSpecifications2.java: -------------------------------------------------------------------------------- 1 | //package com.nooul.apihelpers.springbootrest.specifications; 2 | // 3 | //import jakarta.persistence.criteria.*; 4 | //import org.springframework.data.jpa.domain.Specification; 5 | //import org.springframework.stereotype.Service; 6 | //import org.springframework.util.StringUtils; 7 | // 8 | //import java.lang.reflect.Field; 9 | //import java.time.*; 10 | //import java.util.*; 11 | //import java.util.function.BiFunction; 12 | //import java.util.stream.Collectors; 13 | //@Service 14 | //public class CustomSpecifications2 { 15 | // 16 | // public Specification build(Object filter) { 17 | // if (filter instanceof Map) { 18 | // return buildFromMap((Map) filter, new HashSet<>()); 19 | // } else if (filter instanceof List) { 20 | // List> filters = (List>) filter; 21 | // return filters.stream() 22 | // .map(f -> buildFromMap(f, new HashSet<>())) 23 | // .reduce(Specification::or) 24 | // .orElse(null); 25 | // } 26 | // return null; 27 | // } 28 | // 29 | // private Specification buildFromMap(Map filter, Set joinPaths) { 30 | // return (root, query, cb) -> buildPredicateTree(filter, root, query, cb, joinPaths); 31 | // } 32 | // 33 | // private Predicate buildPredicateTree(Map filter, From root, CriteriaQuery query, CriteriaBuilder cb, Set joinPaths) { 34 | // List predicates = new ArrayList<>(); 35 | // 36 | // for (Map.Entry entry : filter.entrySet()) { 37 | // String rawKey = entry.getKey(); 38 | // Object value = entry.getValue(); 39 | // 40 | // Operator op = Operator.EQ; 41 | // String field = rawKey; 42 | // 43 | // for (Operator o : Operator.values()) { 44 | // if (rawKey.endsWith(o.suffix)) { 45 | // op = o; 46 | // field = rawKey.substring(0, rawKey.length() - o.suffix.length()); 47 | // break; 48 | // } 49 | // } 50 | // 51 | // Path path; 52 | // if (value instanceof Map || value instanceof List) { 53 | // Join join = getOrCreateJoin(root, field, JoinType.LEFT); 54 | // if (value instanceof Map mapValue) { 55 | // predicates.add(buildPredicateTree(mapValue, join, query, cb, joinPaths)); 56 | // } else if (value instanceof List listValue) { 57 | // List orPredicates = listValue.stream() 58 | // .filter(v -> v instanceof Map) 59 | // .map(v -> buildPredicateTree((Map) v, join, query, cb, joinPaths)) 60 | // .toList(); 61 | // predicates.add(cb.or(orPredicates.toArray(new Predicate[0]))); 62 | // } 63 | // continue; 64 | // } 65 | // 66 | // try { 67 | // path = root.get(field); 68 | // } catch (IllegalArgumentException ex) { 69 | // Join join = getOrCreateJoin(root, field, value == null ? JoinType.LEFT : JoinType.INNER); 70 | // path = join.get("id"); 71 | // } 72 | // 73 | // Class type = path.getJavaType(); 74 | // Object typedValue = convertValue(value, type); 75 | // predicates.add(op.predicate(cb, path, typedValue)); 76 | // } 77 | // 78 | // return cb.and(predicates.toArray(new Predicate[0])); 79 | // } 80 | // 81 | // private Join getOrCreateJoin(From root, String field, JoinType joinType) { 82 | // return root.getJoins().stream() 83 | // .filter(j -> j.getAttribute().getName().equals(field)) 84 | // .findFirst() 85 | // .map(j -> (Join) j) 86 | // .orElse(root.join(field, joinType)); 87 | // } 88 | // 89 | // private Object convertValue(Object value, Class type) { 90 | // if (value == null) return null; 91 | // if (type.isInstance(value)) return value; 92 | // 93 | // String str = value.toString(); 94 | // 95 | // try { 96 | // if (type == UUID.class) return UUID.fromString(str); 97 | // if (type == Long.class || type == long.class) return Long.parseLong(str); 98 | // if (type == Integer.class || type == int.class) return Integer.parseInt(str); 99 | // if (type == Instant.class) return Instant.parse(str); 100 | // if (type == LocalDate.class) return LocalDate.parse(str); 101 | // if (type == LocalDateTime.class) return LocalDateTime.parse(str); 102 | // if (type == Boolean.class || type == boolean.class) return Boolean.parseBoolean(str); 103 | // } catch (Exception ignored) { 104 | // } 105 | // 106 | // return str; 107 | // } 108 | // 109 | // enum Operator { 110 | // NOT("Not", (cb, path, val) -> val == null ? cb.isNotNull(path) : cb.notEqual(path, val)), 111 | // GT("Gt", (cb, path, val) -> cb.greaterThan(path.as(Comparable.class), (Comparable) val)), 112 | // LT("Lt", (cb, path, val) -> cb.lessThan(path.as(Comparable.class), (Comparable) val)), 113 | // EQ("", (cb, path, val) -> { 114 | // if (val == null) return cb.isNull(path); 115 | // if (val instanceof String str && str.contains("%")) return cb.like(path.as(String.class), str); 116 | // return cb.equal(path, val); 117 | // }); 118 | // 119 | // final String suffix; 120 | // final TriFunction, Object, Predicate> predicate; 121 | // 122 | // Operator(String suffix, TriFunction, Object, Predicate> predicate) { 123 | // this.suffix = suffix; 124 | // this.predicate = predicate; 125 | // } 126 | // 127 | // Predicate predicate(CriteriaBuilder cb, Path path, Object val) { 128 | // return predicate.apply(cb, path, val); 129 | // } 130 | // } 131 | // 132 | // @FunctionalInterface 133 | // interface TriFunction { 134 | // R apply(A a, B b, C c); 135 | // } 136 | //} 137 | -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/utils/CsvUtils.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.utils; 2 | 3 | import com.fasterxml.jackson.databind.MapperFeature; 4 | import com.fasterxml.jackson.databind.ObjectReader; 5 | import com.fasterxml.jackson.dataformat.csv.CsvMapper; 6 | import com.fasterxml.jackson.dataformat.csv.CsvParser; 7 | import com.fasterxml.jackson.dataformat.csv.CsvSchema; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.util.List; 12 | 13 | public class CsvUtils { 14 | 15 | private CsvUtils() { 16 | 17 | } 18 | 19 | public static List read(Class clazz, InputStream stream, boolean withHeaders, char separator) throws IOException { 20 | CsvMapper mapper = new CsvMapper(); 21 | 22 | mapper.enable(CsvParser.Feature.TRIM_SPACES); 23 | mapper.enable(CsvParser.Feature.ALLOW_TRAILING_COMMA); 24 | mapper.enable(CsvParser.Feature.INSERT_NULLS_FOR_MISSING_COLUMNS); 25 | mapper.enable(CsvParser.Feature.SKIP_EMPTY_LINES); 26 | mapper.disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY); 27 | CsvSchema schema = mapper.schemaFor(clazz).withColumnReordering(true); 28 | ObjectReader reader; 29 | if (separator == '\t') { 30 | schema = schema.withColumnSeparator('\t'); 31 | } 32 | else { 33 | schema = schema.withColumnSeparator(','); 34 | } 35 | if (withHeaders) { 36 | schema = schema.withHeader(); 37 | } 38 | else { 39 | schema = schema.withoutHeader(); 40 | } 41 | reader = mapper.readerFor(clazz).with(schema); 42 | return reader.readValues(stream).readAll(); 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/utils/JSONUtils.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.utils; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.apache.commons.logging.Log; 7 | import org.apache.commons.logging.LogFactory; 8 | import org.json.JSONArray; 9 | import org.json.JSONObject; 10 | 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.IntStream; 17 | 18 | public class JSONUtils { 19 | 20 | protected static final Log logger = LogFactory.getLog(JSONUtils.class); 21 | 22 | public static JSONObject toJsonObject(String str) { 23 | JSONObject jsonObj = new JSONObject(str); 24 | return jsonObj; 25 | } 26 | 27 | 28 | public static JSONArray toJsonArray(String str) { 29 | JSONArray jsonArr = new JSONArray(str); 30 | return jsonArr; 31 | } 32 | 33 | 34 | public static List toList(JSONArray jsonArray) { 35 | return IntStream.range(0,jsonArray.length()).mapToObj(i->jsonArray.get(i)).collect(Collectors.toList()); 36 | } 37 | 38 | public static String toJsonString(Object obj) { 39 | ObjectMapper mapper = new ObjectMapper(); 40 | 41 | //Object to JSON in String 42 | String jsonString = ""; 43 | try { 44 | jsonString = mapper.writeValueAsString(obj); 45 | } catch (JsonProcessingException e) { 46 | logger.error(e); 47 | } 48 | return jsonString; 49 | } 50 | 51 | public static T toObject(String jsonString, Class clazz) { 52 | //JSON from String to Object 53 | ObjectMapper mapper = new ObjectMapper(); 54 | T obj = null; 55 | try { 56 | obj = mapper.readValue(jsonString, clazz); 57 | } catch (IOException e) { 58 | logger.error(e); 59 | } 60 | return obj; 61 | } 62 | 63 | public static List toListOfObjects(String jsonString, Class clazz) { 64 | //JSON from String to Object 65 | ObjectMapper mapper = new ObjectMapper(); 66 | T obj = null; 67 | List listOfObjects = new ArrayList<>(); 68 | try { 69 | T[] objects = mapper.readValue(jsonString, clazz); 70 | listOfObjects = Arrays.asList(objects); 71 | } catch (IOException e) { 72 | logger.error(e); 73 | } 74 | return listOfObjects; 75 | } 76 | 77 | public static boolean isValid(final String json) { 78 | ObjectMapper objectMapper = new ObjectMapper(); 79 | objectMapper.enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY); 80 | boolean valid; 81 | try { 82 | objectMapper.readTree(json); 83 | valid = true; 84 | } catch (IOException e) { 85 | valid = false; 86 | logger.error(e); 87 | } 88 | return valid; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/utils/QueryParamExtractor.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.utils; 2 | 3 | import com.nooul.apihelpers.springbootrest.entities.QueryParamWrapper; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.json.JSONArray; 6 | import org.json.JSONObject; 7 | import org.json.JSONTokener; 8 | 9 | import java.io.UnsupportedEncodingException; 10 | import java.net.URLDecoder; 11 | 12 | public class QueryParamExtractor { 13 | 14 | public static QueryParamWrapper extract(String filterStr, String rangeStr, String sortStr) { 15 | 16 | 17 | 18 | Object filterJsonOrArray; 19 | if (StringUtils.isBlank(filterStr)) { 20 | filterStr = "{}"; 21 | } 22 | 23 | //https://stackoverflow.com/a/18368345 24 | filterStr = filterStr.replaceAll("%(?![0-9a-fA-F]{2})", "%25"); 25 | filterStr = filterStr.replaceAll("\\+", "%2B"); 26 | try { 27 | //https://stackoverflow.com/a/6926987/986160 28 | filterStr = URLDecoder.decode(filterStr.replace("+", "%2B"), "UTF-8") 29 | .replace("%2B", "+"); 30 | } catch (UnsupportedEncodingException e) { 31 | } 32 | 33 | filterJsonOrArray = new JSONTokener(filterStr).nextValue(); 34 | JSONObject filter = null; 35 | JSONArray filterOr = null; 36 | if (filterJsonOrArray instanceof JSONObject) { 37 | filter = JSONUtils.toJsonObject(filterStr); 38 | } 39 | else if (filterJsonOrArray instanceof JSONArray){ 40 | filterOr = JSONUtils.toJsonArray(filterStr); 41 | } 42 | JSONArray range; 43 | if (StringUtils.isBlank(rangeStr)) { 44 | rangeStr = "[]"; 45 | } 46 | range = JSONUtils.toJsonArray(rangeStr); 47 | 48 | JSONArray sort; 49 | if (StringUtils.isBlank(sortStr)) { 50 | sortStr = "[]"; 51 | } 52 | sort = JSONUtils.toJsonArray(sortStr); 53 | 54 | 55 | return new QueryParamWrapper(filter, filterOr, range, sort); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/nooul/apihelpers/springbootrest/utils/UrlUtils.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.utils; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | import java.net.URLDecoder; 5 | import java.net.URLEncoder; 6 | 7 | //https://stackoverflow.com/a/611117/986160 8 | public class UrlUtils { 9 | 10 | public static String decodeURIComponent(String s) 11 | { 12 | if (s == null) 13 | { 14 | return null; 15 | } 16 | 17 | String result = null; 18 | 19 | try 20 | { 21 | result = URLDecoder.decode(s, "UTF-8"); 22 | } 23 | 24 | // This exception should never occur. 25 | catch (UnsupportedEncodingException e) 26 | { 27 | result = s; 28 | } 29 | 30 | return result; 31 | } 32 | 33 | /** 34 | * Encodes the passed String as UTF-8 using an algorithm that's compatible 35 | * with JavaScript's encodeURIComponent function. Returns 36 | * null if the String is null. 37 | * 38 | * @param s The String to be encoded 39 | * @return the encoded String 40 | */ 41 | public static String encodeURIComponent(String s) 42 | { 43 | String result = null; 44 | 45 | try 46 | { 47 | result = URLEncoder.encode(s, "UTF-8") 48 | .replaceAll("\\+", "%20") 49 | .replaceAll("\\%21", "!") 50 | .replaceAll("\\%27", "'") 51 | .replaceAll("\\%28", "(") 52 | .replaceAll("\\%29", ")") 53 | .replaceAll("\\%7E", "~"); 54 | } 55 | 56 | // This exception should never occur. 57 | catch (UnsupportedEncodingException e) 58 | { 59 | result = s; 60 | } 61 | 62 | return result; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/resources/application-test.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | banner-mode: off 4 | h2: 5 | console: 6 | enabled: false 7 | path: /h2 8 | datasource: 9 | driver-class-name: org.h2.Driver 10 | url: jdbc:h2:mem:ab;DB_CLOSE_DELAY=-1 11 | username: sa 12 | password: sa 13 | flyway: 14 | enabled: false 15 | 16 | logging: 17 | level: 18 | root: off 19 | 20 | server: 21 | api-prefix: /api/v1 -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/AbstractJpaDataTest.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest; 2 | 3 | import org.junit.jupiter.api.DisplayNameGeneration; 4 | import org.junit.jupiter.api.DisplayNameGenerator; 5 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 6 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 7 | import org.springframework.test.context.ActiveProfiles; 8 | 9 | @DataJpaTest(showSql = false) 10 | @ActiveProfiles("test") 11 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 12 | @EnableJpaAuditing 13 | public abstract class AbstractJpaDataTest { 14 | } -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/AbstractSpringBootTest.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest; 2 | 3 | import org.junit.jupiter.api.DisplayNameGeneration; 4 | import org.junit.jupiter.api.DisplayNameGenerator; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.web.server.LocalServerPort; 7 | import org.springframework.test.context.ActiveProfiles; 8 | 9 | @ActiveProfiles(profiles = "test") 10 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 11 | classes = {TestApp.class}) 12 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 13 | public abstract class AbstractSpringBootTest { 14 | 15 | @LocalServerPort 16 | protected int port; 17 | 18 | } -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/AbstractUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest; 2 | 3 | import org.junit.jupiter.api.DisplayNameGeneration; 4 | import org.junit.jupiter.api.DisplayNameGenerator; 5 | import org.springframework.test.context.ActiveProfiles; 6 | 7 | @ActiveProfiles(profiles = "test") 8 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 9 | public abstract class AbstractUnitTest { 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/SpringBootRestTest.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class SpringBootRestTest extends AbstractSpringBootTest { 6 | 7 | @Test 8 | public void contextLoads() { 9 | } 10 | 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/TestApp.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class TestApp { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(TestApp.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/controllers/ActorController.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.controllers; 2 | 3 | import com.nooul.apihelpers.springbootrest.entities.QueryParamWrapper; 4 | import com.nooul.apihelpers.springbootrest.helpers.entities.Actor; 5 | import com.nooul.apihelpers.springbootrest.helpers.repositories.ActorRepository; 6 | import com.nooul.apihelpers.springbootrest.services.FilterService; 7 | import com.nooul.apihelpers.springbootrest.utils.QueryParamExtractor; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.Arrays; 15 | 16 | @RestController 17 | @RequestMapping("actors") 18 | public class ActorController { 19 | 20 | 21 | @Autowired 22 | private ActorRepository repository; 23 | 24 | @Autowired 25 | private FilterService filterService; 26 | 27 | 28 | @GetMapping 29 | public Iterable filterBy( 30 | @RequestParam(required = false, name = "filter") String filterStr, 31 | @RequestParam(required = false, name = "range") String rangeStr, @RequestParam(required = false, name = "sort") String sortStr) { 32 | QueryParamWrapper wrapper = QueryParamExtractor.extract(filterStr, rangeStr, sortStr); 33 | return filterService.filterBy(wrapper, repository, Arrays.asList("firstName", "lastName")); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/controllers/CategoryController.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.controllers; 2 | 3 | import com.nooul.apihelpers.springbootrest.entities.QueryParamWrapper; 4 | import com.nooul.apihelpers.springbootrest.helpers.entities.Category; 5 | import com.nooul.apihelpers.springbootrest.helpers.repositories.CategoryRepository; 6 | import com.nooul.apihelpers.springbootrest.services.FilterService; 7 | import com.nooul.apihelpers.springbootrest.utils.QueryParamExtractor; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.Arrays; 15 | 16 | @RestController 17 | @RequestMapping("categories") 18 | public class CategoryController { 19 | 20 | 21 | @Autowired 22 | private CategoryRepository repository; 23 | 24 | @Autowired 25 | private FilterService filterService; 26 | 27 | @GetMapping 28 | public Iterable filterBy( 29 | @RequestParam(required = false, name = "filter") String filterStr, 30 | @RequestParam(required = false, name = "range") String rangeStr, @RequestParam(required = false, name = "sort") String sortStr) { 31 | QueryParamWrapper wrapper = QueryParamExtractor.extract(filterStr, rangeStr, sortStr); 32 | return filterService.filterBy(wrapper, repository, Arrays.asList("name")); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/controllers/DirectorController.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.controllers; 2 | 3 | import com.nooul.apihelpers.springbootrest.entities.QueryParamWrapper; 4 | import com.nooul.apihelpers.springbootrest.helpers.entities.Director; 5 | import com.nooul.apihelpers.springbootrest.helpers.repositories.DirectorRepository; 6 | import com.nooul.apihelpers.springbootrest.services.FilterService; 7 | import com.nooul.apihelpers.springbootrest.utils.QueryParamExtractor; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.Arrays; 15 | 16 | @RestController 17 | @RequestMapping("directors") 18 | public class DirectorController { 19 | 20 | 21 | @Autowired 22 | private DirectorRepository repository; 23 | 24 | @Autowired 25 | private FilterService filterService; 26 | 27 | @GetMapping 28 | public Iterable filterBy( 29 | @RequestParam(required = false, name = "filter") String filterStr, 30 | @RequestParam(required = false, name = "range") String rangeStr, @RequestParam(required = false, name = "sort") String sortStr) { 31 | QueryParamWrapper wrapper = QueryParamExtractor.extract(filterStr, rangeStr, sortStr); 32 | return filterService.filterBy(wrapper, repository, Arrays.asList("firstName", "lastName")); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/controllers/MovieController.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.controllers; 2 | 3 | import com.nooul.apihelpers.springbootrest.entities.QueryParamWrapper; 4 | import com.nooul.apihelpers.springbootrest.helpers.entities.Movie; 5 | import com.nooul.apihelpers.springbootrest.helpers.repositories.MovieRepository; 6 | import com.nooul.apihelpers.springbootrest.services.FilterService; 7 | import com.nooul.apihelpers.springbootrest.utils.QueryParamExtractor; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.Arrays; 15 | 16 | @RestController 17 | @RequestMapping("movies") 18 | public class MovieController { 19 | 20 | 21 | @Autowired 22 | private MovieRepository repository; 23 | 24 | @Autowired 25 | private FilterService filterService; 26 | 27 | @GetMapping 28 | public Iterable filterBy( 29 | @RequestParam(required = false, name = "filter") String filterStr, 30 | @RequestParam(required = false, name = "range") String rangeStr, @RequestParam(required = false, name = "sort") String sortStr) { 31 | QueryParamWrapper wrapper = QueryParamExtractor.extract(filterStr, rangeStr, sortStr); 32 | return filterService.filterBy(wrapper, repository, Arrays.asList("name")); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/controllers/SenderController.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.controllers; 2 | 3 | import com.nooul.apihelpers.springbootrest.entities.QueryParamWrapper; 4 | import com.nooul.apihelpers.springbootrest.helpers.entities.Sender; 5 | import com.nooul.apihelpers.springbootrest.helpers.repositories.SenderRepository; 6 | import com.nooul.apihelpers.springbootrest.services.FilterService; 7 | import com.nooul.apihelpers.springbootrest.utils.QueryParamExtractor; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.Arrays; 15 | import java.util.UUID; 16 | 17 | @RestController 18 | @RequestMapping("senders") 19 | public class SenderController { 20 | @Autowired 21 | private SenderRepository repository; 22 | 23 | @Autowired 24 | FilterService filterService; 25 | 26 | @GetMapping 27 | public Iterable filterBy( 28 | @RequestParam(required = false, name = "filter") String filterStr, 29 | @RequestParam(required = false, name = "range") String rangeStr, @RequestParam(required = false, name = "sort") String sortStr) { 30 | QueryParamWrapper wrapper = QueryParamExtractor.extract(filterStr, rangeStr, sortStr); 31 | return filterService.filterBy(wrapper, repository, "id"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/controllers/UUIDEntityController.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.controllers; 2 | 3 | import com.nooul.apihelpers.springbootrest.entities.QueryParamWrapper; 4 | import com.nooul.apihelpers.springbootrest.helpers.entities.UUIDEntity; 5 | import com.nooul.apihelpers.springbootrest.helpers.repositories.UUIDEntityRepository; 6 | import com.nooul.apihelpers.springbootrest.services.FilterService; 7 | import com.nooul.apihelpers.springbootrest.utils.QueryParamExtractor; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.Arrays; 15 | import java.util.UUID; 16 | 17 | @RestController 18 | @RequestMapping("uuidentity") 19 | public class UUIDEntityController { 20 | @Autowired 21 | private UUIDEntityRepository repository; 22 | 23 | @Autowired 24 | FilterService filterService; 25 | 26 | @GetMapping 27 | public Iterable filterBy( 28 | @RequestParam(required = false, name = "filter") String filterStr, 29 | @RequestParam(required = false, name = "range") String rangeStr, @RequestParam(required = false, name = "sort") String sortStr) { 30 | QueryParamWrapper wrapper = QueryParamExtractor.extract(filterStr, rangeStr, sortStr); 31 | return filterService.filterBy(wrapper, repository, "uuid", Arrays.asList("uuid")); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/controllers/UUIDRelationshipController.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.controllers; 2 | 3 | import com.nooul.apihelpers.springbootrest.entities.QueryParamWrapper; 4 | import com.nooul.apihelpers.springbootrest.helpers.entities.UUIDRelationship; 5 | import com.nooul.apihelpers.springbootrest.helpers.repositories.UUIDRelationshipRepository; 6 | import com.nooul.apihelpers.springbootrest.services.FilterService; 7 | import com.nooul.apihelpers.springbootrest.utils.QueryParamExtractor; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.Arrays; 15 | import java.util.UUID; 16 | 17 | @RestController 18 | @RequestMapping("uuidrelationship") 19 | public class UUIDRelationshipController { 20 | @Autowired 21 | private UUIDRelationshipRepository repository; 22 | 23 | @Autowired 24 | FilterService filterService; 25 | 26 | @GetMapping 27 | public Iterable filterBy( 28 | @RequestParam(required = false, name = "filter") String filterStr, 29 | @RequestParam(required = false, name = "range") String rangeStr, @RequestParam(required = false, name = "sort") String sortStr) { 30 | QueryParamWrapper wrapper = QueryParamExtractor.extract(filterStr, rangeStr, sortStr); 31 | return filterService.filterBy(wrapper, repository, "uuid", Arrays.asList("uuid")); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/entities/Actor.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.entities; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.time.Instant; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | @Entity 13 | @Setter 14 | @Getter 15 | @NoArgsConstructor 16 | public class Actor { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private long id; 21 | 22 | private String firstName; 23 | 24 | private String lastName; 25 | 26 | private int birthYear; 27 | 28 | private Instant birthDate; 29 | 30 | @ManyToMany 31 | private List movies = new ArrayList<>(); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/entities/Category.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.entities; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | import jakarta.persistence.*; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | @Entity 12 | @Setter 13 | @Getter 14 | @NoArgsConstructor 15 | public class Category { 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | private long id; 19 | 20 | private String name; 21 | 22 | @OneToMany(mappedBy = "category") 23 | List movies = new ArrayList<>(); 24 | 25 | @ManyToOne 26 | private Category parentCategory; 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/entities/Director.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.entities; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import jakarta.persistence.*; 7 | 8 | @Entity 9 | @Setter 10 | @Getter 11 | @NoArgsConstructor 12 | public class Director { 13 | 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | private long id; 17 | 18 | private String firstName; 19 | 20 | private String lastName; 21 | 22 | private int birthYear; 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/entities/Movie.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.entities; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | import jakarta.persistence.*; 8 | import java.sql.Timestamp; 9 | import java.time.Instant; 10 | import java.util.ArrayList; 11 | import java.util.HashSet; 12 | import java.util.List; 13 | import java.util.Set; 14 | 15 | @Entity 16 | @Setter 17 | @Getter 18 | @NoArgsConstructor 19 | public class Movie { 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private long id; 23 | 24 | private String name; 25 | 26 | private Timestamp releaseDate; 27 | 28 | private Instant releaseDateInstant; 29 | 30 | @ManyToOne 31 | private Director director; 32 | 33 | @ManyToOne 34 | private Category category; 35 | 36 | @ManyToMany(mappedBy = "movies") 37 | private List actors = new ArrayList<>(); 38 | 39 | @ElementCollection 40 | @CollectionTable(name = "age_ratings") 41 | @Column(name = "age_rating") 42 | private Set ageRatings = new HashSet<>(); 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/entities/Sender.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.entities; 2 | 3 | import com.nooul.apihelpers.springbootrest.helpers.values.Mobile; 4 | import com.nooul.apihelpers.springbootrest.helpers.values.MobileConverter; 5 | import jakarta.persistence.Convert; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.Id; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.Setter; 12 | import org.hibernate.annotations.GenericGenerator; 13 | import org.hibernate.annotations.JdbcTypeCode; 14 | import org.hibernate.annotations.Type; 15 | import org.hibernate.type.SqlTypes; 16 | 17 | import java.util.UUID; 18 | 19 | @Entity 20 | @Getter 21 | @Setter 22 | @NoArgsConstructor 23 | public class Sender { 24 | @Id 25 | @GeneratedValue(generator = "UUID") 26 | @GenericGenerator( 27 | name = "UUID", 28 | strategy = "org.hibernate.id.UUIDGenerator" 29 | ) 30 | private UUID id; 31 | 32 | private String sender; 33 | 34 | @Convert(converter = MobileConverter.class) 35 | private Mobile senderValueObject; 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/entities/UUID.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.entities; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import jakarta.persistence.*; 7 | 8 | @Entity 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | public class UUID { 13 | @Id 14 | private String uuid; 15 | 16 | public UUID(String uuid) { 17 | this.uuid = uuid; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/entities/UUIDEntity.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.entities; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import org.hibernate.annotations.JdbcTypeCode; 7 | import org.hibernate.annotations.Type; 8 | 9 | import jakarta.persistence.*; 10 | import org.hibernate.type.SqlTypes; 11 | 12 | import java.util.UUID; 13 | 14 | @Entity 15 | @Setter 16 | @Getter 17 | @NoArgsConstructor 18 | public class UUIDEntity { 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.AUTO) 21 | @Column(nullable = false, length = 100, updatable = false) 22 | protected UUID uuid; 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/entities/UUIDRelationship.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.entities; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import org.hibernate.annotations.JdbcTypeCode; 7 | import org.hibernate.annotations.Type; 8 | 9 | import jakarta.persistence.*; 10 | import org.hibernate.type.SqlTypes; 11 | 12 | import java.util.UUID; 13 | 14 | @Entity 15 | @Setter 16 | @Getter 17 | @NoArgsConstructor 18 | public class UUIDRelationship { 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.AUTO) 21 | @Column(nullable = false, length = 100, updatable = false) 22 | protected UUID uuid; 23 | 24 | @ManyToOne 25 | @JoinColumn(name = "uuiduuid", referencedColumnName = "uuid") 26 | private UUIDEntity uuidEntity; 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/repositories/ActorRepository.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.repositories; 2 | 3 | import com.nooul.apihelpers.springbootrest.helpers.entities.Actor; 4 | import com.nooul.apihelpers.springbootrest.repositories.BaseRepository; 5 | 6 | public interface ActorRepository extends BaseRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/repositories/CategoryRepository.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.repositories; 2 | 3 | import com.nooul.apihelpers.springbootrest.helpers.entities.Category; 4 | import com.nooul.apihelpers.springbootrest.repositories.BaseRepository; 5 | 6 | public interface CategoryRepository extends BaseRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/repositories/DirectorRepository.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.repositories; 2 | 3 | import com.nooul.apihelpers.springbootrest.helpers.entities.Director; 4 | import com.nooul.apihelpers.springbootrest.repositories.BaseRepository; 5 | 6 | public interface DirectorRepository extends BaseRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/repositories/MovieRepository.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.repositories; 2 | 3 | import com.nooul.apihelpers.springbootrest.helpers.entities.Movie; 4 | import com.nooul.apihelpers.springbootrest.repositories.BaseRepository; 5 | 6 | public interface MovieRepository extends BaseRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/repositories/SenderRepository.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.repositories; 2 | 3 | import com.nooul.apihelpers.springbootrest.helpers.entities.Sender; 4 | import com.nooul.apihelpers.springbootrest.repositories.BaseRepository; 5 | 6 | import java.util.UUID; 7 | 8 | public interface SenderRepository extends BaseRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/repositories/UUIDEntityRepository.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.repositories; 2 | 3 | import com.nooul.apihelpers.springbootrest.helpers.entities.UUIDEntity; 4 | import com.nooul.apihelpers.springbootrest.repositories.BaseRepository; 5 | 6 | import java.util.UUID; 7 | 8 | public interface UUIDEntityRepository extends BaseRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/repositories/UUIDRelationshipRepository.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.repositories; 2 | 3 | import com.nooul.apihelpers.springbootrest.helpers.entities.UUIDRelationship; 4 | import com.nooul.apihelpers.springbootrest.repositories.BaseRepository; 5 | 6 | import java.util.UUID; 7 | 8 | public interface UUIDRelationshipRepository extends BaseRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/utils/DateUtils.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.utils; 2 | 3 | import java.sql.Timestamp; 4 | import java.text.DateFormat; 5 | import java.text.ParseException; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Date; 8 | 9 | public class DateUtils { 10 | public static Timestamp timeStamp(String dateStr) { 11 | DateFormat dateFormat; 12 | if (dateStr.contains("T")) { 13 | dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 14 | } else { 15 | dateFormat = new SimpleDateFormat("yyyy-MM-dd"); 16 | } 17 | Date date; 18 | try { 19 | date = dateFormat.parse(dateStr); 20 | } catch (ParseException e) { 21 | return null; 22 | } 23 | long time = date.getTime(); 24 | return new Timestamp(time); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/values/Mobile.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.values; 2 | 3 | import com.nooul.apihelpers.springbootrest.annotations.ValueObject; 4 | 5 | import java.util.Objects; 6 | 7 | @ValueObject 8 | public class Mobile { 9 | 10 | private String number; 11 | 12 | private Mobile(String number){ 13 | this.number = addPlus(number); 14 | } 15 | 16 | private static String addPlus(String number) { 17 | if(!number.startsWith("+")) { 18 | return "+" + number; 19 | } 20 | return number; 21 | } 22 | 23 | 24 | public static Mobile fromString(String phoneNumber){ 25 | return new Mobile(phoneNumber); 26 | } 27 | 28 | public String formatAsStringWithoutPlus(){ 29 | return this.number.replace("+", ""); 30 | } 31 | 32 | public String formatAsString() { 33 | return this.number; 34 | } 35 | 36 | public static String makeItRegionPrefixed(String phoneNumber) { 37 | 38 | if (phoneNumber.startsWith("+")) { 39 | return phoneNumber; 40 | } 41 | 42 | if (phoneNumber.startsWith("30")) { 43 | return "+" + phoneNumber; 44 | } 45 | 46 | return "+30" + phoneNumber; 47 | } 48 | 49 | @Override 50 | public boolean equals(Object o) { 51 | if (this == o) return true; 52 | if (o == null || getClass() != o.getClass()) return false; 53 | Mobile mobile = (Mobile) o; 54 | return number.equals(mobile.number); 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return Objects.hash(number); 60 | } 61 | 62 | public static String getNumber(Mobile mobile) { 63 | return mobile.formatAsStringWithoutPlus(); 64 | } 65 | } -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/helpers/values/MobileConverter.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.helpers.values; 2 | 3 | 4 | import jakarta.persistence.AttributeConverter; 5 | import jakarta.persistence.Converter; 6 | 7 | @Converter 8 | public class MobileConverter implements AttributeConverter { 9 | @Override 10 | public String convertToDatabaseColumn(Mobile mobile) { 11 | if(mobile == null) { 12 | return null; 13 | } 14 | return mobile.formatAsStringWithoutPlus(); 15 | } 16 | 17 | @Override 18 | public Mobile convertToEntityAttribute(String number) { 19 | if(number == null) { 20 | return null; 21 | } 22 | return Mobile.fromString(number); 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/services/FilterServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.services; 2 | 3 | import com.nooul.apihelpers.springbootrest.AbstractJpaDataTest; 4 | import com.nooul.apihelpers.springbootrest.specifications.CustomSpecifications; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.context.annotation.Import; 9 | 10 | @Import({FilterService.class, CustomSpecifications.class}) 11 | class FilterServiceTest extends AbstractJpaDataTest { 12 | 13 | private FilterService filterService; 14 | 15 | @BeforeEach 16 | void init() { 17 | filterService = new FilterService(); 18 | } 19 | 20 | @Test 21 | void given_a_snake_case_string_conversion_should_succeed() { 22 | String snakeCase = "this_is_a_snake_case"; 23 | String camelCase = filterService.convertToCamelCase(snakeCase); 24 | Assertions.assertEquals("thisIsASnakeCase", camelCase); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/test/java/com/nooul/apihelpers/springbootrest/services/filterByTest.java: -------------------------------------------------------------------------------- 1 | package com.nooul.apihelpers.springbootrest.services; 2 | 3 | import com.nooul.apihelpers.springbootrest.AbstractJpaDataTest; 4 | import com.nooul.apihelpers.springbootrest.helpers.controllers.*; 5 | import com.nooul.apihelpers.springbootrest.helpers.entities.*; 6 | import com.nooul.apihelpers.springbootrest.helpers.repositories.*; 7 | import com.nooul.apihelpers.springbootrest.helpers.values.Mobile; 8 | import com.nooul.apihelpers.springbootrest.specifications.CustomSpecifications; 9 | import org.assertj.core.util.IterableUtil; 10 | import org.junit.jupiter.api.Disabled; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.context.annotation.Import; 14 | 15 | import java.util.Arrays; 16 | import java.util.HashSet; 17 | import java.util.List; 18 | 19 | import static com.nooul.apihelpers.springbootrest.helpers.utils.DateUtils.timeStamp; 20 | import static com.nooul.apihelpers.springbootrest.utils.UrlUtils.encodeURIComponent; 21 | import static org.junit.jupiter.api.Assertions.assertEquals; 22 | 23 | @Import({FilterService.class, CustomSpecifications.class, 24 | MovieController.class, ActorController.class, UUIDEntityController.class, SenderController.class, 25 | UUIDRelationshipController.class 26 | }) 27 | public class filterByTest extends AbstractJpaDataTest { 28 | 29 | @Autowired 30 | private CategoryRepository categoryRepository; 31 | 32 | @Autowired 33 | private MovieRepository movieRepository; 34 | 35 | @Autowired 36 | private ActorRepository actorRepository; 37 | 38 | @Autowired 39 | private DirectorRepository directorRepository; 40 | 41 | @Autowired 42 | private UUIDEntityRepository uuidEntityRepository; 43 | 44 | @Autowired 45 | private SenderRepository senderRepository; 46 | 47 | @Autowired 48 | private UUIDRelationshipRepository uuidRelationshipRepository; 49 | 50 | @Autowired 51 | private MovieController movieController; 52 | 53 | @Autowired 54 | private ActorController actorController; 55 | 56 | @Autowired 57 | private UUIDEntityController uuidEntityController; 58 | 59 | @Autowired 60 | private SenderController senderController; 61 | 62 | @Autowired 63 | private UUIDRelationshipController uuidRelationshipController; 64 | 65 | @Test 66 | public void find_null_primitive_should_return() { 67 | Movie matrix = new Movie(); 68 | matrix.setName("The Matrix"); 69 | matrix.setReleaseDate(timeStamp("1999-01-05")); 70 | movieRepository.save(matrix); 71 | 72 | Movie constantine = new Movie(); 73 | constantine.setName(null); 74 | constantine.setReleaseDate(timeStamp("2005-01-05")); 75 | movieRepository.save(constantine); 76 | 77 | Movie it = new Movie(); 78 | it.setName("IT"); 79 | it.setReleaseDate(timeStamp("2017-01-05")); 80 | movieRepository.save(it); 81 | 82 | Iterable moviesWithNullName1 = movieController.filterBy("{name: null}", null, null); 83 | assertEquals(1, IterableUtil.sizeOf(moviesWithNullName1)); 84 | Iterable moviesWithNullName2 = movieController.filterBy("{name: 'null'}", null, null); 85 | assertEquals(1, IterableUtil.sizeOf(moviesWithNullName2)); 86 | Iterable moviesWithNullName3 = movieController.filterBy("{name: ''}", null, null); 87 | assertEquals(1, IterableUtil.sizeOf(moviesWithNullName3)); 88 | Iterable moviesWithNullName4 = movieController.filterBy("{name: ' '}", null, null); 89 | assertEquals(1, IterableUtil.sizeOf(moviesWithNullName4)); 90 | 91 | Iterable moviesWithNullOrNotNullName1 = movieController.filterBy("[{name: 'The Matrix'},{name: null}]", null, null); 92 | assertEquals(2, IterableUtil.sizeOf(moviesWithNullOrNotNullName1)); 93 | } 94 | 95 | 96 | @Test 97 | public void timestamp_date_range_queries() { 98 | Movie matrix = new Movie(); 99 | matrix.setName("The Matrix"); 100 | matrix.setReleaseDate(timeStamp("1999-01-05")); 101 | movieRepository.save(matrix); 102 | 103 | Movie constantine = new Movie(); 104 | constantine.setName("Constantine"); 105 | constantine.setReleaseDate(timeStamp("2005-01-05")); 106 | movieRepository.save(constantine); 107 | 108 | Movie it = new Movie(); 109 | it.setName("IT"); 110 | it.setReleaseDate(timeStamp("2017-01-05")); 111 | movieRepository.save(it); 112 | 113 | Iterable moviesAfterOrOn2005 = movieController.filterBy("{releaseDateGte: '2005-01-01'}", null, null); 114 | assertEquals(2, IterableUtil.sizeOf(moviesAfterOrOn2005)); 115 | 116 | Iterable moviesAfter2005 = movieController.filterBy("{releaseDateGt: '2005-01-01'}", null, null); 117 | assertEquals(2, IterableUtil.sizeOf(moviesAfter2005)); 118 | 119 | Iterable moviesBeforeOrOn2005 = movieController.filterBy("{releaseDateLte: '2005-01-1'}", null, null); 120 | assertEquals(1, IterableUtil.sizeOf(moviesBeforeOrOn2005)); 121 | 122 | Iterable moviesBefore2005 = movieController.filterBy("{releaseDateLt: '2005-01-01'}", null, null); 123 | assertEquals(1, IterableUtil.sizeOf(moviesBefore2005)); 124 | 125 | Iterable moviesAfter1999Before2017 = movieController.filterBy("{releaseDateGt: '1999-01-01', releaseDateLt: '2017-12-01'}", null, null); 126 | assertEquals(3, IterableUtil.sizeOf(moviesAfter1999Before2017)); 127 | 128 | Iterable moviesAfter2005OrOnBefore2017OrOn = movieController.filterBy("{releaseDateGte: 2005-01-01, releaseDateLte:2017-12-01}", null, null); 129 | assertEquals(2, IterableUtil.sizeOf(moviesAfter2005OrOnBefore2017OrOn)); 130 | } 131 | 132 | @Test 133 | public void instant_date_range_queries() { 134 | Movie matrix = new Movie(); 135 | matrix.setName("The Matrix"); 136 | matrix.setReleaseDateInstant(timeStamp("1999-01-05").toInstant()); 137 | movieRepository.save(matrix); 138 | 139 | Movie constantine = new Movie(); 140 | constantine.setName("Constantine"); 141 | constantine.setReleaseDateInstant(timeStamp("2005-01-05").toInstant()); 142 | movieRepository.save(constantine); 143 | 144 | Movie it = new Movie(); 145 | it.setName("IT"); 146 | it.setReleaseDateInstant(timeStamp("2017-01-05").toInstant()); 147 | movieRepository.save(it); 148 | 149 | Iterable moviesAfterOrOn2005 = movieController.filterBy("{releaseDateInstantGte: '2005-01-01'}", null, null); 150 | assertEquals(2, IterableUtil.sizeOf(moviesAfterOrOn2005)); 151 | 152 | Iterable moviesAfter2005 = movieController.filterBy("{releaseDateInstantGt: '2005-01-01'}", null, null); 153 | assertEquals(2, IterableUtil.sizeOf(moviesAfter2005)); 154 | 155 | Iterable moviesBeforeOrOn2005 = movieController.filterBy("{releaseDateInstantLte: '2005-01-1'}", null, null); 156 | assertEquals(1, IterableUtil.sizeOf(moviesBeforeOrOn2005)); 157 | 158 | Iterable moviesBefore2005 = movieController.filterBy("{releaseDateInstantLt: '2005-01-01'}", null, null); 159 | assertEquals(1, IterableUtil.sizeOf(moviesBefore2005)); 160 | 161 | Iterable moviesAfter1999Before2017 = movieController.filterBy("{releaseDateInstantGt: '1999-01-01', releaseDateInstantLt: '2017-12-01'}", null, null); 162 | assertEquals(3, IterableUtil.sizeOf(moviesAfter1999Before2017)); 163 | 164 | Iterable moviesAfter2005OrOnBefore2017OrOn = movieController.filterBy("{releaseDateInstantGte: 2005-01-01, releaseDateInstantLte:2017-12-01}", null, null); 165 | assertEquals(2, IterableUtil.sizeOf(moviesAfter2005OrOnBefore2017OrOn)); 166 | } 167 | 168 | @Test 169 | public void timestamp_date_with_time_range_queries() { 170 | Movie matrix = new Movie(); 171 | matrix.setName("The Matrix"); 172 | matrix.setReleaseDate(timeStamp("1999-01-05T01:00:00")); 173 | movieRepository.save(matrix); 174 | 175 | Movie constantine = new Movie(); 176 | constantine.setName("Constantine"); 177 | constantine.setReleaseDate(timeStamp("1999-01-05T03:00:00")); 178 | movieRepository.save(constantine); 179 | 180 | Movie it = new Movie(); 181 | it.setName("IT"); 182 | it.setReleaseDate(timeStamp("1999-01-05T04:00:00")); 183 | movieRepository.save(it); 184 | 185 | Iterable moviesAfterOrOn1 = movieController.filterBy("{releaseDateGte: '1999-01-05T01:00:00'}", null, null); 186 | assertEquals(3, IterableUtil.sizeOf(moviesAfterOrOn1)); 187 | 188 | Iterable moviesAfter1 = movieController.filterBy("{releaseDateGt: '1999-01-05T01:00:00'}", null, null); 189 | assertEquals(2, IterableUtil.sizeOf(moviesAfter1)); 190 | 191 | Iterable moviesAfterOrOn3 = movieController.filterBy("{releaseDateGte: '1999-01-05T03:00:00'}", null, null); 192 | assertEquals(2, IterableUtil.sizeOf(moviesAfterOrOn3)); 193 | 194 | Iterable moviesAfter3 = movieController.filterBy("{releaseDateGt: '1999-01-05T03:00:00'}", null, null); 195 | assertEquals(1, IterableUtil.sizeOf(moviesAfter3)); 196 | 197 | } 198 | 199 | @Test 200 | public void instant_date_with_time_range_queries() { 201 | Movie matrix = new Movie(); 202 | matrix.setName("The Matrix"); 203 | matrix.setReleaseDateInstant(timeStamp("1999-01-05T01:00:00").toInstant()); 204 | movieRepository.save(matrix); 205 | 206 | Movie constantine = new Movie(); 207 | constantine.setName("Constantine"); 208 | constantine.setReleaseDateInstant(timeStamp("1999-01-05T03:00:00").toInstant()); 209 | movieRepository.save(constantine); 210 | 211 | Movie it = new Movie(); 212 | it.setName("IT"); 213 | it.setReleaseDateInstant(timeStamp("1999-01-05T04:00:00").toInstant()); 214 | movieRepository.save(it); 215 | 216 | Iterable moviesAfterOrOn1 = movieController.filterBy("{releaseDateInstantGte: '1999-01-05T01:00:00'}", null, null); 217 | assertEquals(3, IterableUtil.sizeOf(moviesAfterOrOn1)); 218 | 219 | Iterable moviesAfter1 = movieController.filterBy("{releaseDateInstantGt: '1999-01-05T01:00:00'}", null, null); 220 | assertEquals(2, IterableUtil.sizeOf(moviesAfter1)); 221 | 222 | Iterable moviesAfterOrOn3 = movieController.filterBy("{releaseDateInstantGte: '1999-01-05T03:00:00'}", null, null); 223 | assertEquals(2, IterableUtil.sizeOf(moviesAfterOrOn3)); 224 | 225 | Iterable moviesAfter3 = movieController.filterBy("{releaseDateInstantGt: '1999-01-05T03:00:00'}", null, null); 226 | assertEquals(1, IterableUtil.sizeOf(moviesAfter3)); 227 | 228 | } 229 | 230 | 231 | @Test 232 | public void date_with_implied_zero_time_in_range_queries() { 233 | Movie matrix = new Movie(); 234 | matrix.setName("The Matrix"); 235 | matrix.setReleaseDate(timeStamp("1999-01-05T01:00:00")); 236 | movieRepository.save(matrix); 237 | 238 | Movie constantine = new Movie(); 239 | constantine.setName("Constantine"); 240 | constantine.setReleaseDate(timeStamp("1999-01-05T03:00:00")); 241 | movieRepository.save(constantine); 242 | 243 | Movie it = new Movie(); 244 | it.setName("IT"); 245 | it.setReleaseDate(timeStamp("1999-01-06T01:00:00")); 246 | movieRepository.save(it); 247 | 248 | 249 | Movie it2 = new Movie(); 250 | it2.setName("IT2"); 251 | it2.setReleaseDate(timeStamp("1999-01-07T00:00:00")); 252 | movieRepository.save(it2); 253 | 254 | Iterable moviesBeforeOrOnSixth = movieController.filterBy("{releaseDateLte: '1999-01-06'}", null, null); 255 | assertEquals(2, IterableUtil.sizeOf(moviesBeforeOrOnSixth)); 256 | 257 | Iterable moviesBeforeSixth = movieController.filterBy("{releaseDateLt: '1999-01-06'}", null, null); 258 | assertEquals(2, IterableUtil.sizeOf(moviesBeforeSixth)); 259 | 260 | Iterable moviesAfterOrOnSixth = movieController.filterBy("{releaseDateGte: '1999-01-06'}", null, null); 261 | assertEquals(2, IterableUtil.sizeOf(moviesAfterOrOnSixth)); 262 | 263 | Iterable moviesAfterSixth = movieController.filterBy("{releaseDateGt: '1999-01-06'}", null, null); 264 | assertEquals(2, IterableUtil.sizeOf(moviesAfterSixth)); 265 | 266 | Iterable moviesBetweenSixthAndSeventh = movieController.filterBy("{releaseDateGt: '1999-01-06', releaseDateLt: '1999-01-07'}", null, null); 267 | assertEquals(1, IterableUtil.sizeOf(moviesBetweenSixthAndSeventh)); 268 | 269 | Iterable moviesBetweenSixthAndSeventhIncluded = movieController.filterBy("{releaseDateGte: '1999-01-06', releaseDateLte: '1999-01-07'}", null, null); 270 | assertEquals(2, IterableUtil.sizeOf(moviesBetweenSixthAndSeventhIncluded)); 271 | 272 | Iterable moviesBetweenFifthAndSeventh = movieController.filterBy("{releaseDateGt: '1999-01-05', releaseDateLt: '1999-01-07'}", null, null); 273 | assertEquals(3, IterableUtil.sizeOf(moviesBetweenFifthAndSeventh)); 274 | 275 | Iterable moviesBetweenFifthAndSeventhIncluded = movieController.filterBy("{releaseDateGte: '1999-01-05', releaseDateLte: '1999-01-07'}", null, null); 276 | assertEquals(4, IterableUtil.sizeOf(moviesBetweenFifthAndSeventhIncluded)); 277 | } 278 | 279 | @Test 280 | public void string_range_queries() { 281 | 282 | Movie constantine = new Movie(); 283 | constantine.setName("Constantine"); 284 | constantine.setReleaseDate(timeStamp("2005-01-05")); 285 | movieRepository.save(constantine); 286 | 287 | Movie it = new Movie(); 288 | it.setName("IT"); 289 | it.setReleaseDate(timeStamp("2017-01-05")); 290 | movieRepository.save(it); 291 | 292 | 293 | Movie matrix = new Movie(); 294 | matrix.setName("The Matrix"); 295 | matrix.setReleaseDate(timeStamp("1999-01-05")); 296 | movieRepository.save(matrix); 297 | 298 | 299 | Iterable moviesAfterA = movieController.filterBy("{nameGt: A }", null, null); 300 | assertEquals(3, IterableUtil.sizeOf(moviesAfterA)); 301 | 302 | Iterable moviesBeforeD = movieController.filterBy("{nameLt: D }", null, null); 303 | assertEquals(1, IterableUtil.sizeOf(moviesBeforeD)); 304 | 305 | Iterable moviesAfterDBeforeM = movieController.filterBy("{nameGt: D, nameLt:M }", null, null); 306 | assertEquals(1, IterableUtil.sizeOf(moviesAfterDBeforeM)); 307 | 308 | } 309 | 310 | @Test 311 | public void reference_many_to_one_null__fetch_movies_with_no_director() { 312 | Director lana = new Director(); 313 | lana.setFirstName("Lana"); 314 | lana.setLastName("Wachowski"); 315 | directorRepository.save(lana); 316 | 317 | 318 | Movie matrix = new Movie(); 319 | matrix.setName("The Matrix"); 320 | matrix.setDirector(lana); 321 | movieRepository.save(matrix); 322 | 323 | 324 | Movie constantine = new Movie(); 325 | constantine.setName("Constantine"); 326 | movieRepository.save(constantine); 327 | 328 | Movie it = new Movie(); 329 | it.setName("IT"); 330 | movieRepository.save(it); 331 | 332 | 333 | Iterable noDirectorMovies = movieController.filterBy("{director: null}", null, null); 334 | assertEquals(2, IterableUtil.sizeOf(noDirectorMovies)); 335 | } 336 | 337 | @Test 338 | public void reference_many_to_one_null__fetch_movies_with_category_having_parent_category() { 339 | 340 | Category fiction = new Category(); 341 | fiction.setName("fiction"); 342 | categoryRepository.save(fiction); 343 | 344 | Category horror = new Category(); 345 | horror.setName("horror"); 346 | horror.setParentCategory(fiction); 347 | categoryRepository.save(horror); 348 | 349 | 350 | Movie matrix = new Movie(); 351 | matrix.setName("The Matrix"); 352 | matrix.setCategory(fiction); 353 | movieRepository.save(matrix); 354 | 355 | Movie constantine = new Movie(); 356 | constantine.setName("Constantine"); 357 | constantine.setCategory(horror); 358 | movieRepository.save(constantine); 359 | 360 | Movie it = new Movie(); 361 | it.setName("IT"); 362 | it.setCategory(horror); 363 | movieRepository.save(it); 364 | 365 | Iterable moviesWithCategoriesThatHaveParentCategoryHorror1 = movieController.filterBy("{category: {parentCategory: {id:" + fiction.getId() + "}}}", null, null); 366 | assertEquals(2, IterableUtil.sizeOf(moviesWithCategoriesThatHaveParentCategoryHorror1)); 367 | Iterable moviesWithCategoriesThatHaveParentCategoryHorror2 = movieController.filterBy("{category: {parentCategory: " + fiction.getId() + "}}", null, null); 368 | assertEquals(2, IterableUtil.sizeOf(moviesWithCategoriesThatHaveParentCategoryHorror2)); 369 | } 370 | 371 | 372 | @Test 373 | @Disabled("hibernate 6 always passes distinct so allowDuplicates: true is not supported right now - https://docs.jboss.org/hibernate/orm/6.0/migration-guide/migration-guide.html#query-sqm-distinct") 374 | public void reference_many_to_one_null__fetch_movies_with_category_having_parent_category_with_duplicates() { 375 | 376 | Category fiction = new Category(); 377 | fiction.setName("fiction"); 378 | categoryRepository.save(fiction); 379 | 380 | Category horror = new Category(); 381 | horror.setName("horror"); 382 | horror.setParentCategory(fiction); 383 | categoryRepository.save(horror); 384 | 385 | 386 | Movie matrix = new Movie(); 387 | matrix.setName("The Matrix"); 388 | matrix.setCategory(fiction); 389 | movieRepository.save(matrix); 390 | 391 | Movie constantine = new Movie(); 392 | constantine.setName("Constantine"); 393 | constantine.setCategory(horror); 394 | movieRepository.save(constantine); 395 | 396 | Movie it = new Movie(); 397 | it.setName("IT"); 398 | it.setCategory(horror); 399 | movieRepository.save(it); 400 | 401 | Iterable moviesWithCategoriesThatHaveParentCategoryHorror = movieController.filterBy("{category: {parentCategory: " + fiction.getId() + "}, allowDuplicates: true }}", null, null); 402 | assertEquals(4, IterableUtil.sizeOf(moviesWithCategoriesThatHaveParentCategoryHorror)); 403 | 404 | 405 | } 406 | 407 | @Test 408 | public void reference_many_to_one_null__fetch_movies_with_director_not_null() { 409 | Director lana = new Director(); 410 | lana.setFirstName("Lana"); 411 | lana.setLastName("Wachowski"); 412 | directorRepository.save(lana); 413 | 414 | 415 | Director delRay = new Director(); 416 | lana.setFirstName("del"); 417 | lana.setLastName("Ray"); 418 | directorRepository.save(delRay); 419 | 420 | 421 | Movie matrix = new Movie(); 422 | matrix.setName("The Matrix"); 423 | matrix.setDirector(lana); 424 | movieRepository.save(matrix); 425 | 426 | 427 | Movie constantine = new Movie(); 428 | constantine.setName("Constantine"); 429 | movieRepository.save(constantine); 430 | 431 | Movie it = new Movie(); 432 | it.setName("IT"); 433 | movieRepository.save(it); 434 | 435 | Movie delRayClip = new Movie(); 436 | delRayClip.setName("del ray"); 437 | delRayClip.setDirector(delRay); 438 | movieRepository.save(delRayClip); 439 | 440 | Iterable directorNotNullMovies = movieController.filterBy("{directorNot: null}", null, null); 441 | assertEquals(2, IterableUtil.sizeOf(directorNotNullMovies)); 442 | 443 | Iterable directorNullMovies = movieController.filterBy("{director: null}", null, null); 444 | assertEquals(2, IterableUtil.sizeOf(directorNullMovies)); 445 | 446 | Iterable directorLanaOrDelRay = movieController.filterBy("[{director:"+lana.getId()+"}, { director: "+delRay.getId()+" }]", null, null); 447 | assertEquals(2, IterableUtil.sizeOf(directorLanaOrDelRay)); 448 | } 449 | 450 | @Test 451 | public void special_case_combining_null_with_first_level_associated_id_in_or_operation() { 452 | Director lana = new Director(); 453 | lana.setFirstName("Lana"); 454 | lana.setLastName("Wachowski"); 455 | directorRepository.save(lana); 456 | 457 | 458 | Director delRay = new Director(); 459 | lana.setFirstName("del"); 460 | lana.setLastName("Ray"); 461 | directorRepository.save(delRay); 462 | 463 | 464 | Movie matrix = new Movie(); 465 | matrix.setName("The Matrix"); 466 | matrix.setDirector(lana); 467 | movieRepository.save(matrix); 468 | 469 | 470 | Movie constantine = new Movie(); 471 | constantine.setName("Constantine"); 472 | movieRepository.save(constantine); 473 | 474 | Movie it = new Movie(); 475 | it.setName("IT"); 476 | movieRepository.save(it); 477 | 478 | Movie delRayClip = new Movie(); 479 | delRayClip.setName("del ray"); 480 | delRayClip.setDirector(delRay); 481 | movieRepository.save(delRayClip); 482 | 483 | 484 | 485 | Iterable directorNullOrLanaMovies = movieController.filterBy("{ director:["+lana.getId()+",null]}", null, null); 486 | assertEquals(3, IterableUtil.sizeOf(directorNullOrLanaMovies)); 487 | 488 | Iterable directorNullOrLanaMovies2 = movieController.filterBy("{ director:[null,"+lana.getId()+"]}", null, null); 489 | assertEquals(3, IterableUtil.sizeOf(directorNullOrLanaMovies2)); 490 | 491 | Iterable directorNullOrLanaMovies3 = movieController.filterBy("{ director:[null,{id: "+lana.getId()+"}]}", null, null); 492 | assertEquals(3, IterableUtil.sizeOf(directorNullOrLanaMovies3)); 493 | 494 | Iterable directorNullOrLanaMovies4 = movieController.filterBy("[{ director:null}, {director: "+lana.getId()+"}]", null, null); 495 | assertEquals(3, IterableUtil.sizeOf(directorNullOrLanaMovies4)); 496 | 497 | Iterable directorNullOrLanaMovies5 = movieController.filterBy("[{ director:null}, {director: {id: "+lana.getId()+"}}]", null, null); 498 | assertEquals(3, IterableUtil.sizeOf(directorNullOrLanaMovies5)); 499 | 500 | 501 | } 502 | 503 | @Test 504 | public void reference_many_to_many_null__fetch_actors_with_no_movies() { 505 | Movie matrix = new Movie(); 506 | matrix.setName("The Matrix"); 507 | movieRepository.save(matrix); 508 | 509 | Movie constantine = new Movie(); 510 | constantine.setName("Constantine"); 511 | movieRepository.save(constantine); 512 | 513 | Movie it = new Movie(); 514 | it.setName("IT"); 515 | movieRepository.save(it); 516 | 517 | Actor keanu = new Actor(); 518 | keanu.setFirstName("Keanu"); 519 | keanu.setLastName("Reeves"); 520 | keanu.setMovies(Arrays.asList(matrix, constantine)); 521 | actorRepository.save(keanu); 522 | 523 | Actor noMovieActor = new Actor(); 524 | noMovieActor.setFirstName("No Movie"); 525 | noMovieActor.setLastName("Whatsoever"); 526 | actorRepository.save(noMovieActor); 527 | 528 | 529 | Actor noMovieActor2 = new Actor(); 530 | noMovieActor2.setFirstName("No Movie"); 531 | noMovieActor2.setLastName("Whatsoever 2"); 532 | actorRepository.save(noMovieActor2); 533 | 534 | 535 | Iterable noMovieActors = actorController.filterBy("{movies: null}", null, null); 536 | assertEquals(2, IterableUtil.sizeOf(noMovieActors)); 537 | } 538 | 539 | @Test 540 | public void reference_many_to_many_not_null__fetch_actors_with_at_least_a_movie() { 541 | Movie matrix = new Movie(); 542 | matrix.setName("The Matrix"); 543 | movieRepository.save(matrix); 544 | 545 | Movie constantine = new Movie(); 546 | constantine.setName("Constantine"); 547 | movieRepository.save(constantine); 548 | 549 | Movie it = new Movie(); 550 | it.setName("IT"); 551 | movieRepository.save(it); 552 | 553 | Actor keanu = new Actor(); 554 | keanu.setFirstName("Keanu"); 555 | keanu.setLastName("Reeves"); 556 | keanu.setMovies(Arrays.asList(matrix, constantine)); 557 | actorRepository.save(keanu); 558 | 559 | Actor carrie = new Actor(); 560 | carrie.setFirstName("Carrie-Anne"); 561 | carrie.setLastName("Moss"); 562 | carrie.setMovies(Arrays.asList(matrix)); 563 | actorRepository.save(carrie); 564 | 565 | Actor noMovieActor = new Actor(); 566 | noMovieActor.setFirstName("No Movie"); 567 | noMovieActor.setLastName("Whatsoever"); 568 | actorRepository.save(noMovieActor); 569 | 570 | 571 | Actor noMovieActor2 = new Actor(); 572 | noMovieActor2.setFirstName("No Movie"); 573 | noMovieActor2.setLastName("Whatsoever 2"); 574 | actorRepository.save(noMovieActor2); 575 | 576 | 577 | Iterable withMovieActors = actorController.filterBy("{moviesNot: null}", null, null); 578 | assertEquals(2, IterableUtil.sizeOf(withMovieActors)); 579 | } 580 | 581 | @Disabled("fails due to performance issues fix and is not supported") 582 | @Test 583 | public void reference_test_conjunctive_equality_in_list__fetch_actors_that_have_played_in_all_movies_of_query() { 584 | Movie matrix = new Movie(); 585 | matrix.setName("The Matrix"); 586 | movieRepository.save(matrix); 587 | 588 | Movie constantine = new Movie(); 589 | constantine.setName("Constantine"); 590 | movieRepository.save(constantine); 591 | 592 | Movie it = new Movie(); 593 | it.setName("IT"); 594 | movieRepository.save(it); 595 | 596 | Actor keanu = new Actor(); 597 | keanu.setFirstName("Keanu"); 598 | keanu.setLastName("Reeves"); 599 | keanu.setMovies(Arrays.asList(matrix, constantine)); 600 | actorRepository.save(keanu); 601 | 602 | Actor carrie = new Actor(); 603 | carrie.setFirstName("Carrie-Anne"); 604 | carrie.setLastName("Moss"); 605 | carrie.setMovies(Arrays.asList(matrix)); 606 | actorRepository.save(carrie); 607 | 608 | Actor noMovieActor = new Actor(); 609 | noMovieActor.setFirstName("No Movie"); 610 | noMovieActor.setLastName("Whatsoever"); 611 | actorRepository.save(noMovieActor); 612 | 613 | 614 | Actor noMovieActor2 = new Actor(); 615 | noMovieActor2.setFirstName("No Movie"); 616 | noMovieActor2.setLastName("Whatsoever 2"); 617 | actorRepository.save(noMovieActor2); 618 | 619 | 620 | Iterable matrixAndConstantineActors = actorController.filterBy("{moviesAnd: [" + matrix.getId() + "," + constantine.getId() + "]}", null, null); 621 | assertEquals(1, IterableUtil.sizeOf(matrixAndConstantineActors)); 622 | } 623 | 624 | @Test 625 | public void reference_match__fetch_movie_by_actor_id() { 626 | Movie matrix = new Movie(); 627 | matrix.setName("The Matrix"); 628 | movieRepository.save(matrix); 629 | 630 | Movie constantine = new Movie(); 631 | constantine.setName("Constantine"); 632 | movieRepository.save(constantine); 633 | 634 | Movie it = new Movie(); 635 | it.setName("IT"); 636 | movieRepository.save(it); 637 | 638 | Actor keanu = new Actor(); 639 | keanu.setFirstName("Keanu"); 640 | keanu.setLastName("Reeves"); 641 | keanu.setMovies(Arrays.asList(matrix, constantine)); 642 | actorRepository.save(keanu); 643 | 644 | 645 | Iterable keanuMovies = movieController.filterBy("{actors: {id: " + keanu.getId() + "}}", null, null); 646 | Iterable keanuMovies2 = movieController.filterBy("{actors: " + keanu.getId() + "}", null, null); 647 | Iterable keanuMovies3 = movieController.filterBy("{actors: [{id: " + keanu.getId() + "}]}", null, null); 648 | assertEquals(2, IterableUtil.sizeOf(keanuMovies)); 649 | assertEquals(2, IterableUtil.sizeOf(keanuMovies2)); 650 | assertEquals(2, IterableUtil.sizeOf(keanuMovies3)); 651 | } 652 | 653 | @Test 654 | public void disjunctive_reference_match__fetch_movie_by_multiple_actor_ids() { 655 | Movie matrix = new Movie(); 656 | matrix.setName("The Matrix"); 657 | movieRepository.save(matrix); 658 | 659 | Movie constantine = new Movie(); 660 | constantine.setName("Constantine"); 661 | movieRepository.save(constantine); 662 | 663 | Movie it = new Movie(); 664 | it.setName("IT"); 665 | movieRepository.save(it); 666 | 667 | Actor keanu = new Actor(); 668 | keanu.setFirstName("Keanu"); 669 | keanu.setLastName("Reeves"); 670 | keanu.setMovies(Arrays.asList(matrix, constantine)); 671 | actorRepository.save(keanu); 672 | 673 | Actor jaeden = new Actor(); 674 | jaeden.setFirstName("Jaeden"); 675 | jaeden.setLastName("Martell"); 676 | jaeden.setMovies(Arrays.asList(it)); 677 | actorRepository.save(jaeden); 678 | 679 | 680 | Iterable moviesByActors = movieController.filterBy("{actors: [" + keanu.getId() + ", " + jaeden.getId() + "]}", null, null); 681 | 682 | assertEquals(3, IterableUtil.sizeOf(moviesByActors)); 683 | } 684 | 685 | @Test 686 | public void fetch_by_multiple_ids__fetch_movies_by_ids() { 687 | Movie matrix = new Movie(); 688 | matrix.setName("The Matrix"); 689 | movieRepository.save(matrix); 690 | 691 | Movie constantine = new Movie(); 692 | constantine.setName("Constantine"); 693 | movieRepository.save(constantine); 694 | 695 | Movie it = new Movie(); 696 | it.setName("IT"); 697 | movieRepository.save(it); 698 | 699 | Iterable moviesById = movieController.filterBy("{ id: [" + matrix.getId() + "," + constantine.getId() + "]}", null, null); 700 | assertEquals(2, IterableUtil.sizeOf(moviesById)); 701 | } 702 | 703 | @Test 704 | public void fetch_by_multiple_ids__fetch_movies_by_not_including_ids() { 705 | Movie matrix = new Movie(); 706 | matrix.setName("The Matrix"); 707 | movieRepository.save(matrix); 708 | 709 | Movie constantine = new Movie(); 710 | constantine.setName("Constantine"); 711 | movieRepository.save(constantine); 712 | 713 | Movie it = new Movie(); 714 | it.setName("IT"); 715 | movieRepository.save(it); 716 | 717 | Iterable moviesByNotIds = movieController.filterBy("{ idNot: [" + matrix.getId() + "," + constantine.getId() + "]}", null, null); 718 | assertEquals(1, IterableUtil.sizeOf(moviesByNotIds)); 719 | } 720 | 721 | @Test 722 | public void fetch_by_id__fetch_movie_by_id() { 723 | Movie matrix = new Movie(); 724 | matrix.setName("The Matrix"); 725 | movieRepository.save(matrix); 726 | 727 | Movie constantine = new Movie(); 728 | constantine.setName("Constantine"); 729 | movieRepository.save(constantine); 730 | 731 | Movie it = new Movie(); 732 | it.setName("IT"); 733 | movieRepository.save(it); 734 | 735 | 736 | Iterable movieById = movieController.filterBy("{id:" + matrix.getId() + "}", null, null); 737 | assertEquals(1, IterableUtil.sizeOf(movieById)); 738 | } 739 | 740 | @Test 741 | public void fetch_by_id__fetch_movie_by_id_and_name_with_or_on_first_level() { 742 | Movie matrix = new Movie(); 743 | matrix.setName("The Matrix"); 744 | movieRepository.save(matrix); 745 | 746 | Movie constantine = new Movie(); 747 | constantine.setName("Constantine"); 748 | movieRepository.save(constantine); 749 | 750 | Movie it = new Movie(); 751 | it.setName("IT"); 752 | movieRepository.save(it); 753 | 754 | 755 | Iterable movieById = movieController.filterBy("[{id:" + matrix.getId() + "},{id:" + constantine.getId() + "}]", null, null); 756 | assertEquals(2, IterableUtil.sizeOf(movieById)); 757 | Iterable movieByTwoNames = movieController.filterBy("[{name:" + matrix.getName() + "},{name:" + constantine.getName() + "}]", null, null); 758 | assertEquals(2, IterableUtil.sizeOf(movieByTwoNames)); 759 | Iterable movieByTwoNames2 = movieController.filterBy("{name:[" + matrix.getName() + "," + constantine.getName() + "]}", null, null); 760 | assertEquals(2, IterableUtil.sizeOf(movieByTwoNames2)); 761 | Iterable movieByTwoNamesOneWrong = movieController.filterBy("[{name:" + matrix.getName() + "},{name:somethingsomething}]", null, null); 762 | assertEquals(1, IterableUtil.sizeOf(movieByTwoNamesOneWrong)); 763 | Iterable movieByTwoNamesTwoWrong = movieController.filterBy("[{name:something},{name:somethingsomething}]", null, null); 764 | assertEquals(0, IterableUtil.sizeOf(movieByTwoNamesTwoWrong)); 765 | Iterable movieByIdOrName = movieController.filterBy("[{id:" + matrix.getId() + "},{name:" + constantine.getName() + "}]", null, null); 766 | assertEquals(2, IterableUtil.sizeOf(movieByIdOrName)); 767 | Iterable movieByIdOrName2 = movieController.filterBy("[{id:" + constantine.getId() + "},{name:" + constantine.getName() + "}]", null, null); 768 | assertEquals(1, IterableUtil.sizeOf(movieByIdOrName2)); 769 | } 770 | 771 | @Test 772 | public void fetch_by_id__fetch_movie_by_not_id() { 773 | Movie matrix = new Movie(); 774 | matrix.setName("The Matrix"); 775 | movieRepository.save(matrix); 776 | 777 | Movie constantine = new Movie(); 778 | constantine.setName("Constantine"); 779 | movieRepository.save(constantine); 780 | 781 | Movie it = new Movie(); 782 | it.setName("IT"); 783 | movieRepository.save(it); 784 | 785 | 786 | Iterable movieByNotId = movieController.filterBy("{idNot:" + matrix.getId() + "}", null, null); 787 | assertEquals(2, IterableUtil.sizeOf(movieByNotId)); 788 | } 789 | 790 | @Test 791 | public void exact_match_of_primitive__fetch_movie_by_name() { 792 | Movie matrix = new Movie(); 793 | matrix.setName("The Matrix"); 794 | movieRepository.save(matrix); 795 | 796 | Movie constantine = new Movie(); 797 | constantine.setName("Constantine"); 798 | movieRepository.save(constantine); 799 | 800 | Movie it = new Movie(); 801 | it.setName("IT"); 802 | movieRepository.save(it); 803 | 804 | Iterable movieByName = movieController.filterBy("{name:" + matrix.getName() + "}", null, null); 805 | assertEquals(1, IterableUtil.sizeOf(movieByName)); 806 | } 807 | 808 | 809 | @Test 810 | public void exact_match_of_primitive_in_primitive_collection__fetch_movie_by_age_rating() { 811 | 812 | Movie matrix = new Movie(); 813 | matrix.setName("The Matrix"); 814 | matrix.setAgeRatings(new HashSet<>(Arrays.asList("PG-13"))); 815 | movieRepository.save(matrix); 816 | 817 | Movie matrix2 = new Movie(); 818 | matrix2.setName("The Matrix Reloaded"); 819 | matrix2.setAgeRatings(new HashSet<>(Arrays.asList("R"))); 820 | movieRepository.save(matrix2); 821 | 822 | Movie constantine = new Movie(); 823 | constantine.setName("Constantine"); 824 | constantine.setAgeRatings(new HashSet<>(Arrays.asList("R"))); 825 | movieRepository.save(constantine); 826 | 827 | Iterable movieByAgeRating = movieController.filterBy("{ageRatings: R}", null, null); 828 | assertEquals(2, IterableUtil.sizeOf(movieByAgeRating)); 829 | } 830 | 831 | 832 | @Test 833 | 834 | public void two_level_many_to_many_fetch_movies_with_actor_id() { 835 | Movie matrix = new Movie(); 836 | matrix.setName("The Matrix"); 837 | movieRepository.save(matrix); 838 | 839 | Movie constantine = new Movie(); 840 | constantine.setName("Constantine"); 841 | movieRepository.save(constantine); 842 | 843 | Movie it = new Movie(); 844 | it.setName("IT"); 845 | movieRepository.save(it); 846 | 847 | Actor keanu = new Actor(); 848 | keanu.setFirstName("Keanu"); 849 | keanu.setLastName("Reeves"); 850 | keanu.setMovies(Arrays.asList(matrix, constantine)); 851 | actorRepository.save(keanu); 852 | 853 | 854 | Iterable keanuMovies = movieController.filterBy("{actors: {id:" + keanu.getId() + "}}", null, null); 855 | assertEquals(2, IterableUtil.sizeOf(keanuMovies)); 856 | } 857 | 858 | @Test 859 | 860 | public void two_level_many_to_many_fetch_movies_with_actor_first_name() { 861 | Movie matrix = new Movie(); 862 | matrix.setName("The Matrix"); 863 | movieRepository.save(matrix); 864 | 865 | Movie constantine = new Movie(); 866 | constantine.setName("Constantine"); 867 | movieRepository.save(constantine); 868 | 869 | Movie it = new Movie(); 870 | it.setName("IT"); 871 | movieRepository.save(it); 872 | 873 | Actor keanu = new Actor(); 874 | keanu.setFirstName("Keanu"); 875 | keanu.setLastName("Reeves"); 876 | keanu.setMovies(Arrays.asList(matrix, constantine)); 877 | actorRepository.save(keanu); 878 | 879 | Iterable keanuMovies = movieController.filterBy("{actors: {firstName:" + keanu.getFirstName() + ", lastNameNot: Reves}}", null, null); 880 | assertEquals(2, IterableUtil.sizeOf(keanuMovies)); 881 | } 882 | 883 | @Test 884 | 885 | public void two_level_many_to_many_fetch_movies_with_actor_first_name_and_id_overrides() { 886 | Movie matrix = new Movie(); 887 | matrix.setName("The Matrix"); 888 | movieRepository.save(matrix); 889 | 890 | Movie constantine = new Movie(); 891 | constantine.setName("Constantine"); 892 | movieRepository.save(constantine); 893 | 894 | Movie it = new Movie(); 895 | it.setName("IT"); 896 | movieRepository.save(it); 897 | 898 | Actor keanu = new Actor(); 899 | keanu.setFirstName("Keanu"); 900 | keanu.setLastName("Reeves"); 901 | keanu.setMovies(Arrays.asList(matrix, constantine)); 902 | actorRepository.save(keanu); 903 | 904 | Iterable keanuMovies = movieController.filterBy("{actors: {id: " + keanu.getId() + ", firstName:" + keanu.getFirstName() + ", lastNameNot: Reves}}", null, null); 905 | assertEquals(2, IterableUtil.sizeOf(keanuMovies)); 906 | Iterable constantineMovie = movieController.filterBy("{id: " + constantine.getId() + ", actors: {id: " + keanu.getId() + ", firstName:" + keanu.getFirstName() + ", lastNameNot: Reves}}", null, null); 907 | assertEquals(1, IterableUtil.sizeOf(constantineMovie)); 908 | } 909 | 910 | @Test 911 | 912 | public void two_level_many_to_many_fetch_movies_with_actor_first_name_like() { 913 | Movie matrix = new Movie(); 914 | matrix.setName("The Matrix"); 915 | movieRepository.save(matrix); 916 | 917 | Movie constantine = new Movie(); 918 | constantine.setName("Constantine"); 919 | movieRepository.save(constantine); 920 | 921 | Movie it = new Movie(); 922 | it.setName("IT"); 923 | movieRepository.save(it); 924 | 925 | Actor keanu = new Actor(); 926 | keanu.setFirstName("Keanu"); 927 | keanu.setLastName("Reeves"); 928 | keanu.setMovies(Arrays.asList(matrix, constantine)); 929 | actorRepository.save(keanu); 930 | 931 | Iterable keanuMovies = movieController.filterBy(encodeURIComponent("{actors: {firstName:%ean%, lastName: %eeve%}}"), null, null); 932 | assertEquals(2, IterableUtil.sizeOf(keanuMovies)); 933 | } 934 | 935 | @Test 936 | 937 | public void two_level_many_to_one_fetch_movies_with_director_first_name_exact() { 938 | 939 | Director lana = new Director(); 940 | lana.setFirstName("Lana"); 941 | lana.setLastName("Wachowski"); 942 | directorRepository.save(lana); 943 | 944 | Director other = new Director(); 945 | other.setFirstName("other"); 946 | other.setLastName("Other"); 947 | directorRepository.save(other); 948 | 949 | Movie matrix = new Movie(); 950 | matrix.setName("The Matrix"); 951 | matrix.setDirector(lana); 952 | movieRepository.save(matrix); 953 | 954 | Movie constantine = new Movie(); 955 | constantine.setName("Constantine"); 956 | constantine.setDirector(other); 957 | movieRepository.save(constantine); 958 | 959 | Movie it = new Movie(); 960 | it.setName("IT"); 961 | it.setDirector(other); 962 | movieRepository.save(it); 963 | 964 | Actor keanu = new Actor(); 965 | keanu.setFirstName("Keanu"); 966 | keanu.setLastName("Reeves"); 967 | keanu.setMovies(Arrays.asList(matrix, constantine)); 968 | actorRepository.save(keanu); 969 | 970 | Iterable lanaMovies = movieController.filterBy("{director: {firstName:Lana}}", null, null); 971 | assertEquals(1, IterableUtil.sizeOf(lanaMovies)); 972 | } 973 | 974 | 975 | @Test 976 | 977 | public void three_level_fetch_actors_of_movies_with_director_first_name_exact() { 978 | 979 | Director lana = new Director(); 980 | lana.setFirstName("Lana"); 981 | lana.setLastName("Wachowski"); 982 | directorRepository.save(lana); 983 | 984 | Director other = new Director(); 985 | other.setFirstName("other"); 986 | other.setLastName("Other"); 987 | directorRepository.save(other); 988 | 989 | Movie matrix = new Movie(); 990 | matrix.setName("The Matrix"); 991 | matrix.setDirector(lana); 992 | movieRepository.save(matrix); 993 | 994 | Movie constantine = new Movie(); 995 | constantine.setName("Constantine"); 996 | constantine.setDirector(other); 997 | movieRepository.save(constantine); 998 | 999 | Movie it = new Movie(); 1000 | it.setName("IT"); 1001 | it.setDirector(other); 1002 | movieRepository.save(it); 1003 | 1004 | Actor keanu = new Actor(); 1005 | keanu.setFirstName("Keanu"); 1006 | keanu.setLastName("Reeves"); 1007 | keanu.setMovies(Arrays.asList(matrix, constantine)); 1008 | actorRepository.save(keanu); 1009 | 1010 | Actor otherActor = new Actor(); 1011 | otherActor.setFirstName("other"); 1012 | otherActor.setLastName("other"); 1013 | actorRepository.save(otherActor); 1014 | 1015 | Iterable actors = actorController.filterBy("{movies: {director: {firstName:Lana}}}", null, null); 1016 | assertEquals(1, IterableUtil.sizeOf(actors)); 1017 | } 1018 | 1019 | 1020 | @Test 1021 | 1022 | public void three_level_fetch_actors_of_movies_with_director_first_name_like() { 1023 | 1024 | Director lana = new Director(); 1025 | lana.setFirstName("Lana"); 1026 | lana.setLastName("Wachowski"); 1027 | directorRepository.save(lana); 1028 | 1029 | Director other = new Director(); 1030 | other.setFirstName("other"); 1031 | other.setLastName("Other"); 1032 | directorRepository.save(other); 1033 | 1034 | Movie matrix = new Movie(); 1035 | matrix.setName("The Matrix"); 1036 | matrix.setDirector(lana); 1037 | movieRepository.save(matrix); 1038 | 1039 | Movie constantine = new Movie(); 1040 | constantine.setName("Constantine"); 1041 | constantine.setDirector(other); 1042 | movieRepository.save(constantine); 1043 | 1044 | Movie it = new Movie(); 1045 | it.setName("IT"); 1046 | it.setDirector(other); 1047 | movieRepository.save(it); 1048 | 1049 | Actor keanu = new Actor(); 1050 | keanu.setFirstName("Keanu"); 1051 | keanu.setLastName("Reeves"); 1052 | keanu.setMovies(Arrays.asList(matrix, constantine)); 1053 | actorRepository.save(keanu); 1054 | 1055 | Actor otherActor = new Actor(); 1056 | otherActor.setFirstName("other"); 1057 | otherActor.setLastName("other"); 1058 | actorRepository.save(otherActor); 1059 | 1060 | Iterable actors = actorController.filterBy("{movies: {director: {firstName:Lan%}}}", null, null); 1061 | assertEquals(1, IterableUtil.sizeOf(actors)); 1062 | } 1063 | 1064 | 1065 | @Test 1066 | 1067 | public void two_level_many_to_many_fetch_movies_with_actor_having_firstName_and_last_name_in_three_equivalent_ways() { 1068 | Movie matrix = new Movie(); 1069 | matrix.setName("The Matrix"); 1070 | movieRepository.save(matrix); 1071 | 1072 | Movie constantine = new Movie(); 1073 | constantine.setName("Constantine"); 1074 | movieRepository.save(constantine); 1075 | 1076 | Movie it = new Movie(); 1077 | it.setName("IT"); 1078 | movieRepository.save(it); 1079 | 1080 | Actor keanu = new Actor(); 1081 | keanu.setFirstName("Keanu"); 1082 | keanu.setLastName("Reeves"); 1083 | keanu.setMovies(Arrays.asList(matrix, constantine)); 1084 | actorRepository.save(keanu); 1085 | 1086 | Iterable keanuMovies = movieController.filterBy("{actors: {firstName:Keanu, lastName: Reeves}}", null, null); 1087 | assertEquals(2, IterableUtil.sizeOf(keanuMovies)); 1088 | 1089 | Iterable keanuMovies2 = movieController.filterBy("{actorsAnd: [{firstName:Keanu},{lastName: Reeves}]}", null, null); 1090 | assertEquals(2, IterableUtil.sizeOf(keanuMovies2)); 1091 | Iterable keanuMovies3 = movieController.filterBy("{actors: [{firstName:SomethingSomething},{lastName: Reeves}]}", null, null); 1092 | assertEquals(2, IterableUtil.sizeOf(keanuMovies3)); 1093 | Iterable keanuMovies4 = movieController.filterBy("{actorsAnd: [{firstName:SomethingSomething},{lastName: Reeves}]}", null, null); 1094 | assertEquals(0, IterableUtil.sizeOf(keanuMovies4)); 1095 | } 1096 | 1097 | @Disabled("can't support it right now") 1098 | @Test 1099 | public void two_level_many_to_many_fetch_actors_with_movies_starting_with_matr_or_const() { 1100 | Movie matrix = new Movie(); 1101 | matrix.setName("The Matrix"); 1102 | movieRepository.save(matrix); 1103 | 1104 | Movie constantine = new Movie(); 1105 | constantine.setName("Constantine"); 1106 | movieRepository.save(constantine); 1107 | 1108 | Movie it = new Movie(); 1109 | it.setName("IT"); 1110 | movieRepository.save(it); 1111 | 1112 | Actor keanu = new Actor(); 1113 | keanu.setFirstName("Keanu"); 1114 | keanu.setLastName("Reeves"); 1115 | keanu.setMovies(Arrays.asList(matrix, constantine)); 1116 | actorRepository.save(keanu); 1117 | 1118 | Actor noMovieActor = new Actor(); 1119 | noMovieActor.setFirstName("No Movie"); 1120 | noMovieActor.setLastName("Whatsoever"); 1121 | actorRepository.save(noMovieActor); 1122 | 1123 | Actor noMovieActor2 = new Actor(); 1124 | noMovieActor2.setFirstName("No Movie 2"); 1125 | noMovieActor2.setLastName("Whatsoever 2"); 1126 | actorRepository.save(noMovieActor2); 1127 | 1128 | 1129 | Iterable actors = actorController.filterBy(encodeURIComponent("{movies: [{name:%atr%},{name:%onest%}]}}"), null, null); 1130 | assertEquals(1, IterableUtil.sizeOf(actors)); 1131 | Iterable actors2 = actorController.filterBy(encodeURIComponent("{moviesAnd: [{name:%atr%},{name:%onst%}]}}"), null, null); 1132 | assertEquals(1, IterableUtil.sizeOf(actors2)); 1133 | Iterable actors3 = actorController.filterBy(encodeURIComponent("{moviesAnd: [{name:%atr%},{name:%onest%}]}}"), null, null); 1134 | assertEquals(0, IterableUtil.sizeOf(actors3)); 1135 | } 1136 | 1137 | @Test //fails due to performance issues fix and is not supported 1138 | public void two_level_exact_match_of_primitive__fetch_movie_by_director_name() { 1139 | 1140 | Director lana = new Director(); 1141 | lana.setFirstName("Lana"); 1142 | lana.setLastName("Wachowski"); 1143 | directorRepository.save(lana); 1144 | 1145 | Movie matrix = new Movie(); 1146 | matrix.setName("The Matrix"); 1147 | matrix.setDirector(lana); 1148 | movieRepository.save(matrix); 1149 | 1150 | Movie matrix2 = new Movie(); 1151 | matrix2.setName("The Matrix Reloaded"); 1152 | matrix2.setDirector(lana); 1153 | movieRepository.save(matrix2); 1154 | 1155 | Movie constantine = new Movie(); 1156 | constantine.setName("Constantine"); 1157 | movieRepository.save(constantine); 1158 | 1159 | 1160 | Iterable movieByName = movieController.filterBy("{director: {firstName:" + lana.getFirstName() + "}}", null, null); 1161 | assertEquals(2, IterableUtil.sizeOf(movieByName)); 1162 | } 1163 | 1164 | @Test 1165 | public void two_level_full_text_search__fetch_movie_like_director_name() { 1166 | 1167 | Director lana = new Director(); 1168 | lana.setFirstName("Lana"); 1169 | lana.setLastName("Wachowski"); 1170 | directorRepository.save(lana); 1171 | 1172 | Movie matrix = new Movie(); 1173 | matrix.setName("The Matrix"); 1174 | matrix.setDirector(lana); 1175 | movieRepository.save(matrix); 1176 | 1177 | Movie matrix2 = new Movie(); 1178 | matrix2.setName("The Matrix Reloaded"); 1179 | matrix2.setDirector(lana); 1180 | movieRepository.save(matrix2); 1181 | 1182 | Movie constantine = new Movie(); 1183 | constantine.setName("Constantine"); 1184 | movieRepository.save(constantine); 1185 | 1186 | 1187 | Iterable movieByName = movieController.filterBy(encodeURIComponent("{director: {firstName:%an%}}"), null, null); 1188 | assertEquals(2, IterableUtil.sizeOf(movieByName)); 1189 | } 1190 | 1191 | @Test 1192 | public void two_level__fetch_movie_like_director_name() { 1193 | 1194 | Director lana = new Director(); 1195 | lana.setFirstName("Lana"); 1196 | lana.setLastName("Wachowski"); 1197 | directorRepository.save(lana); 1198 | 1199 | Movie matrix = new Movie(); 1200 | matrix.setName("The Matrix"); 1201 | matrix.setDirector(lana); 1202 | movieRepository.save(matrix); 1203 | 1204 | Movie matrix2 = new Movie(); 1205 | matrix2.setName("The Matrix Reloaded"); 1206 | matrix2.setDirector(lana); 1207 | movieRepository.save(matrix2); 1208 | 1209 | Movie constantine = new Movie(); 1210 | constantine.setName("Constantine"); 1211 | movieRepository.save(constantine); 1212 | 1213 | Iterable movieByName = movieController.filterBy(encodeURIComponent("{director: {firstName:%an%}}"), null, null); 1214 | assertEquals(2, IterableUtil.sizeOf(movieByName)); 1215 | } 1216 | 1217 | 1218 | @Test 1219 | public void two_level__fetch_movie_with_director_id() { 1220 | 1221 | Director lana = new Director(); 1222 | lana.setFirstName("Lana"); 1223 | lana.setLastName("Wachowski"); 1224 | directorRepository.save(lana); 1225 | 1226 | Movie matrix = new Movie(); 1227 | matrix.setName("The Matrix"); 1228 | matrix.setDirector(lana); 1229 | movieRepository.save(matrix); 1230 | 1231 | Movie matrix2 = new Movie(); 1232 | matrix2.setName("The Matrix Reloaded"); 1233 | matrix2.setDirector(lana); 1234 | movieRepository.save(matrix2); 1235 | 1236 | Movie constantine = new Movie(); 1237 | constantine.setName("Constantine"); 1238 | movieRepository.save(constantine); 1239 | 1240 | Iterable movieByName = movieController.filterBy("{director: {id:" + lana.getId() + "}}", null, null); 1241 | assertEquals(2, IterableUtil.sizeOf(movieByName)); 1242 | } 1243 | 1244 | @Test 1245 | public void search_text_on_primitive__fetch_movie_by_name_prefix() { 1246 | Movie matrix = new Movie(); 1247 | matrix.setName("The Matrix"); 1248 | movieRepository.save(matrix); 1249 | 1250 | Movie constantine = new Movie(); 1251 | constantine.setName("The Matrix: Reloaded"); 1252 | movieRepository.save(constantine); 1253 | 1254 | Movie it = new Movie(); 1255 | it.setName("IT"); 1256 | movieRepository.save(it); 1257 | 1258 | Iterable movieByName = movieController.filterBy(encodeURIComponent("{name: The Matr%}"), null, null); 1259 | assertEquals(2, IterableUtil.sizeOf(movieByName)); 1260 | } 1261 | 1262 | @Test 1263 | public void search_text_on_primitive__fetch_movie_by_name_postfix() { 1264 | Movie matrix = new Movie(); 1265 | matrix.setName("The Matrix"); 1266 | movieRepository.save(matrix); 1267 | 1268 | Movie constantine = new Movie(); 1269 | constantine.setName("The Matrix: Reloaded"); 1270 | movieRepository.save(constantine); 1271 | 1272 | Movie it = new Movie(); 1273 | it.setName("IT"); 1274 | movieRepository.save(it); 1275 | 1276 | Iterable movieByName = movieController.filterBy(encodeURIComponent("{name: %loaded}"), null, null); 1277 | assertEquals(1, IterableUtil.sizeOf(movieByName)); 1278 | } 1279 | 1280 | @Test 1281 | public void search_text_on_primitive__fetch_movie_by_name_infix() { 1282 | Movie matrix = new Movie(); 1283 | matrix.setName("The Matrix"); 1284 | movieRepository.save(matrix); 1285 | 1286 | Movie constantine = new Movie(); 1287 | constantine.setName("The Matrix: Reloaded"); 1288 | movieRepository.save(constantine); 1289 | 1290 | Movie it = new Movie(); 1291 | it.setName("IT"); 1292 | movieRepository.save(it); 1293 | 1294 | Iterable movieByName = movieController.filterBy(encodeURIComponent("{name: %atri%}"), null, null); 1295 | assertEquals(2, IterableUtil.sizeOf(movieByName)); 1296 | } 1297 | 1298 | @Test 1299 | public void search_text_on_primitive__fetch_movie_by_not_containing_name_prefix() { 1300 | Movie matrix = new Movie(); 1301 | matrix.setName("The Matrix"); 1302 | movieRepository.save(matrix); 1303 | 1304 | Movie constantine = new Movie(); 1305 | constantine.setName("The Matrix: Reloaded"); 1306 | movieRepository.save(constantine); 1307 | 1308 | Movie it = new Movie(); 1309 | it.setName("IT"); 1310 | movieRepository.save(it); 1311 | 1312 | Iterable movieByName = movieController.filterBy(encodeURIComponent("{nameNot: The Matr%}"), null, null); 1313 | assertEquals(1, IterableUtil.sizeOf(movieByName)); 1314 | } 1315 | 1316 | @Test 1317 | public void filter_by_primary_key_that_is_native_uuid() { 1318 | UUIDEntity entity1 = new UUIDEntity(); 1319 | uuidEntityRepository.save(entity1); 1320 | 1321 | UUIDEntity entity2 = new UUIDEntity(); 1322 | uuidEntityRepository.save(entity2); 1323 | 1324 | Iterable entitiesByUuid = uuidEntityController.filterBy("{uuid: " + entity1.getUuid() + "}", null, null); 1325 | assertEquals(1, IterableUtil.sizeOf(entitiesByUuid)); 1326 | } 1327 | 1328 | @Test 1329 | public void search_by_part_of_a_uuid_field() { 1330 | Sender sender1 = new Sender(); 1331 | sender1.setSender("306970011222"); 1332 | senderRepository.save(sender1); 1333 | 1334 | Sender sender2 = new Sender(); 1335 | sender2.setSender("306970011333"); 1336 | senderRepository.save(sender2); 1337 | 1338 | String partOfUuid = sender1.getId().toString().substring(10, 20); 1339 | 1340 | Iterable entitiesByUuid = senderController.filterBy("{q: " + partOfUuid + "}", null, null); 1341 | assertEquals(1, IterableUtil.sizeOf(entitiesByUuid)); 1342 | } 1343 | 1344 | @Test 1345 | public void search_by_part_of_a_mobile_field_which_is_integer_as_input_bug_fix() { 1346 | Sender sender1 = new Sender(); 1347 | sender1.setSender("306970011222"); 1348 | senderRepository.save(sender1); 1349 | 1350 | Sender sender2 = new Sender(); 1351 | sender2.setSender("306970011333"); 1352 | senderRepository.save(sender2); 1353 | 1354 | Iterable entitiesByMobile = senderController.filterBy("{q: 69700112}", null, null); 1355 | assertEquals(1, IterableUtil.sizeOf(entitiesByMobile)); 1356 | } 1357 | 1358 | @Test 1359 | public void search_by_part_of_value_object_field() { 1360 | Sender sender1 = new Sender(); 1361 | sender1.setSenderValueObject(Mobile.fromString("306970011444")); 1362 | senderRepository.save(sender1); 1363 | 1364 | Sender sender2 = new Sender(); 1365 | sender2.setSenderValueObject(Mobile.fromString("306970011555")); 1366 | senderRepository.save(sender2); 1367 | 1368 | Iterable entitiesByMobile = senderController.filterBy("{q: 306970011555 }", null, null); 1369 | assertEquals(1, IterableUtil.sizeOf(entitiesByMobile)); 1370 | } 1371 | 1372 | @Test 1373 | public void value_object_field_exact_match_null_and_inequality() { 1374 | Sender sender1 = new Sender(); 1375 | sender1.setSenderValueObject(Mobile.fromString("306970011123")); 1376 | senderRepository.save(sender1); 1377 | 1378 | Sender sender2 = new Sender(); 1379 | sender2.setSenderValueObject(Mobile.fromString("306970032123")); 1380 | senderRepository.save(sender2); 1381 | 1382 | 1383 | Sender sender3 = new Sender(); 1384 | senderRepository.save(sender3); 1385 | 1386 | Sender sender4 = new Sender(); 1387 | sender4.setSenderValueObject(Mobile.fromString("3069722222222")); 1388 | senderRepository.save(sender4); 1389 | 1390 | Iterable entitiesByValueObject = senderController.filterBy("{senderValueObject: 306970011123 }", null, null); 1391 | assertEquals(1, IterableUtil.sizeOf(entitiesByValueObject)); 1392 | 1393 | Iterable entitiesByNullValueObject = senderController.filterBy("{senderValueObject: null }", null, null); 1394 | assertEquals(0, IterableUtil.sizeOf(entitiesByNullValueObject)); 1395 | 1396 | Iterable entitiesByNotNullValueObject = senderController.filterBy("{senderValueObjectNot: null }", null, null); 1397 | assertEquals(3, IterableUtil.sizeOf(entitiesByNotNullValueObject)); 1398 | 1399 | Iterable entitiesByNotValueObject = senderController.filterBy("{senderValueObjectNot: [306970011123,null] }", null, null); 1400 | assertEquals(2, IterableUtil.sizeOf(entitiesByNotValueObject)); 1401 | } 1402 | 1403 | @Test 1404 | public void filter_by_foreign_key_that_is_native_uuid() { 1405 | UUIDEntity entity1 = new UUIDEntity(); 1406 | uuidEntityRepository.save(entity1); 1407 | 1408 | UUIDEntity entity2 = new UUIDEntity(); 1409 | uuidEntityRepository.save(entity2); 1410 | 1411 | UUIDRelationship relationship1 = new UUIDRelationship(); 1412 | relationship1.setUuidEntity(entity1); 1413 | uuidRelationshipRepository.save(relationship1); 1414 | 1415 | UUIDRelationship relationship2 = new UUIDRelationship(); 1416 | relationship2.setUuidEntity(entity1); 1417 | uuidRelationshipRepository.save(relationship2); 1418 | 1419 | UUIDRelationship relationship3 = new UUIDRelationship(); 1420 | relationship3.setUuidEntity(entity2); 1421 | uuidRelationshipRepository.save(relationship3); 1422 | 1423 | Iterable relsByUuid = uuidRelationshipController.filterBy("{uuidEntity:" + entity1.getUuid() + " }", null, null); 1424 | Iterable relsByUuid2 = uuidRelationshipController.filterBy("{uuidEntity: {uuid: " + entity1.getUuid() + " }}", null, null); 1425 | assertEquals(2, IterableUtil.sizeOf(relsByUuid)); 1426 | assertEquals(2, IterableUtil.sizeOf(relsByUuid2)); 1427 | } 1428 | 1429 | @Test 1430 | public void search_text_on_primitive__fetch_movie_by_not_containing_name_postfix() { 1431 | Movie matrix = new Movie(); 1432 | matrix.setName("The Matrix"); 1433 | movieRepository.save(matrix); 1434 | 1435 | Movie constantine = new Movie(); 1436 | constantine.setName("The Matrix: Reloaded"); 1437 | movieRepository.save(constantine); 1438 | 1439 | Movie it = new Movie(); 1440 | it.setName("IT"); 1441 | movieRepository.save(it); 1442 | 1443 | Iterable movieByName = movieController.filterBy(encodeURIComponent("{nameNot: %loaded}"), null, null); 1444 | assertEquals(2, IterableUtil.sizeOf(movieByName)); 1445 | } 1446 | 1447 | @Test 1448 | public void search_text_on_primitive__fetch_movie_by_not_oontaining_name_infix() { 1449 | Movie matrix = new Movie(); 1450 | matrix.setName("The Matrix"); 1451 | movieRepository.save(matrix); 1452 | 1453 | Movie constantine = new Movie(); 1454 | constantine.setName("The Matrix: Reloaded"); 1455 | movieRepository.save(constantine); 1456 | 1457 | Movie it = new Movie(); 1458 | it.setName("IT"); 1459 | movieRepository.save(it); 1460 | 1461 | Iterable movieByName = movieController.filterBy(encodeURIComponent("{nameNot: %atri%}"), null, null); 1462 | assertEquals(1, IterableUtil.sizeOf(movieByName)); 1463 | } 1464 | 1465 | 1466 | @Test 1467 | public void exact_match_of_primitive__fetch_movie_by_not_name() { 1468 | Movie matrix = new Movie(); 1469 | matrix.setName("The Matrix"); 1470 | movieRepository.save(matrix); 1471 | 1472 | Movie constantine = new Movie(); 1473 | constantine.setName("Constantine"); 1474 | movieRepository.save(constantine); 1475 | 1476 | Movie it = new Movie(); 1477 | it.setName("IT"); 1478 | movieRepository.save(it); 1479 | 1480 | Iterable movieByNotName = movieController.filterBy("{nameNot:" + matrix.getName() + "}", null, null); 1481 | assertEquals(2, IterableUtil.sizeOf(movieByNotName)); 1482 | } 1483 | 1484 | @Test 1485 | public void disjunctive_exact_match_of_primitives__fetch_movie_by_list_of_names() { 1486 | Movie matrix = new Movie(); 1487 | matrix.setName("The Matrix"); 1488 | movieRepository.save(matrix); 1489 | 1490 | Movie constantine = new Movie(); 1491 | constantine.setName("Constantine"); 1492 | movieRepository.save(constantine); 1493 | 1494 | Movie it = new Movie(); 1495 | it.setName("IT"); 1496 | movieRepository.save(it); 1497 | 1498 | Iterable moviesByName = movieController.filterBy("{name: [" + matrix.getName() + "," + constantine.getName() + "]}", null, null); 1499 | assertEquals(2, IterableUtil.sizeOf(moviesByName)); 1500 | } 1501 | 1502 | 1503 | @Test 1504 | public void full_text_search_in_all_fields() { 1505 | Movie matrix = new Movie(); 1506 | matrix.setName("The Matrix"); 1507 | movieRepository.save(matrix); 1508 | 1509 | Movie constantine = new Movie(); 1510 | constantine.setName("Constantine"); 1511 | movieRepository.save(constantine); 1512 | 1513 | Movie it = new Movie(); 1514 | it.setName("IT"); 1515 | movieRepository.save(it); 1516 | 1517 | Iterable movieByFullText = movieController.filterBy("{q:atr}", null, null); 1518 | assertEquals(1, IterableUtil.sizeOf(movieByFullText)); 1519 | } 1520 | 1521 | @Test 1522 | public void find_all_works() { 1523 | Movie matrix = new Movie(); 1524 | matrix.setName("The Matrix"); 1525 | movieRepository.save(matrix); 1526 | 1527 | Movie constantine = new Movie(); 1528 | constantine.setName("Constantine"); 1529 | movieRepository.save(constantine); 1530 | 1531 | Movie it = new Movie(); 1532 | it.setName("IT"); 1533 | movieRepository.save(it); 1534 | 1535 | Iterable allMovies = movieController.filterBy("{}", null, null); 1536 | assertEquals(3, IterableUtil.sizeOf(allMovies)); 1537 | Iterable allMovies2 = movieController.filterBy(null, null, null); 1538 | assertEquals(3, IterableUtil.sizeOf(allMovies2)); 1539 | } 1540 | } 1541 | --------------------------------------------------------------------------------