├── .README_images ├── specification1.png └── specificationO.png ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── SAMPLE.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── github │ │ └── ozayduman │ │ └── specificationbuilder │ │ ├── Joinable.java │ │ ├── SpecificationMappings.java │ │ ├── SpecificationOperator.java │ │ ├── dto │ │ ├── CriteriaDTO.java │ │ ├── Operator.java │ │ ├── PageRequestDTO.java │ │ ├── PageResultDTO.java │ │ ├── RangeDTO.java │ │ ├── operation │ │ │ ├── AbstractOperation.java │ │ │ ├── MultiValueOperation.java │ │ │ ├── NoValueOperation.java │ │ │ ├── RangeValueOperation.java │ │ │ ├── SingleValueOperation.java │ │ │ └── package-info.java │ │ └── package-info.java │ │ └── package-info.java └── resources │ └── application.properties └── test ├── java └── com │ └── github │ └── ozayduman │ └── specificationbuilder │ ├── SpecificationBuilderIntegrationTest.java │ ├── TestConfiguration.java │ ├── TestDataGenerator.java │ ├── TestUtil.java │ ├── dto │ ├── EmployeeResponseDTO.java │ ├── PageRequestDTOTest.java │ ├── PageResultDTOTest.java │ └── operation │ │ ├── AbstractOperationTest.java │ │ ├── MultiValueOperationTest.java │ │ ├── NoValueOperationTest.java │ │ ├── RangeValueOperationTest.java │ │ └── SingleValueOperationTest.java │ ├── entity │ ├── Employee.java │ ├── Phone.java │ ├── PhoneType.java │ ├── SocialSecurity.java │ └── SocialSecurityType.java │ ├── mapper │ └── EmployeeMapper.java │ └── repository │ └── EmployeeRepository.java └── resources └── application.properties /.README_images/specification1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozayduman/spring-data-specification-builder/a6dfaa51d9fece19706561cc52ce12c187cedec5/.README_images/specification1.png -------------------------------------------------------------------------------- /.README_images/specificationO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozayduman/spring-data-specification-builder/a6dfaa51d9fece19706561cc52ce12c187cedec5/.README_images/specificationO.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozayduman/spring-data-specification-builder/a6dfaa51d9fece19706561cc52ce12c187cedec5/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Specification Builder 2 | [![GitHub license](https://img.shields.io/github/license/ozayduman/spring-data-specification-builder)](https://github.com/ozayduman/spring-data-specification-builder/blob/main/LICENSE) 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.ozayduman/specification-builder/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.ozayduman/specification-builder) 4 | [![JavaDoc](https://javadoc-badge.appspot.com/com.github.ozayduman/specification-builder.svg?label=javadoc)](https://javadoc.io/doc/com.github.ozayduman/specification-builder/latest/index.html) 5 | [![GitHub issues](https://img.shields.io/github/issues/ozayduman/spring-data-specification-builder)](https://github.com/ozayduman/spring-data-specification-builder/issues) 6 | [![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Fozay_duman)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fozayduman%2Fspring-data-specification-builder) 7 | 8 | Specification-Builder is a client-oriented dynamic search query library that supports joins among multiple tables in a strongly-type manner for Spring Projects. 9 | This library simplifies writing type-safe queries for search screens by using `Spring Data JPA`'s `JpaSpecificationExecutor` and `hibernate-jpamodelgen`. 10 | As you might know foreach query screen you have to pass a specific DTO (Data Transfer Objects) and write specific queries using that DTO. 11 | This leads to boiler-plate, useless, repetitive code. By using this library you can get rid of that kind of code, and write fluent-style dynamic queries driven by client-side easily. 12 | 13 | #### FEATURES 14 | * Client-oriented dynamic query generation by using fluent style programming. 15 | * You can use different properties for the client and the server-side. This feature enables us not to expose domain entities to external world directly. 16 | * You can restrict, and open individual properties for query operations. 17 | * You can use the same property names for both client-side and server-side. 18 | * You can combine the dynamic query generation with your custom specifications. 19 | * Client-side decides to what operations will take place depending on the operands put in the `criteriaDTO` or `pageRequestDTO`. On the client-side you can use the following operators: 20 | * equal to: `EQ` 21 | * not equal to: `NOT_EQ` 22 | * greater than: `GT` 23 | * greater than or equal to: `GE` 24 | * less than`LT` 25 | * less than or equal to : `LE` 26 | * between : `BT` 27 | * in : `IN` 28 | * not in : `NOT_IN` 29 | * is null: `NULL` 30 | * is not null: `NOT_NULL` 31 | * is true: `TRUE` 32 | * is false: `FALSE` 33 | * like: `LIKE` 34 | * not like: `NOT_LIKE` 35 | * You can use all these operators also in joins if needed as well. 36 | #### DOCUMENTATION 37 | * [User Guide](#server-side) 38 | * [Javadoc](https://javadoc.io/doc/com.github.ozayduman/specification-builder/latest/index.html) 39 | * [Sample Project](https://github.com/ozayduman/spring-data-specification) 40 | #### HOW TO USE 41 | Just add the following maven dependency to your pom.xml file. 42 | ```` 43 | 44 | com.github.ozayduman 45 | specification-builder 46 | 0.0.5 47 | 48 | ```` 49 | For Gradle, use the following dependency: 50 | ```` 51 | implementation 'com.github.ozayduman:specification-builder:0.0.5' 52 | ```` 53 | For Scala SBT, use the following dependency: 54 | ```` 55 | libraryDependencies += "com.github.ozayduman" % "specification-builder" % "0.0.5" 56 | ```` 57 | #### USAGE 58 | 59 | #### SERVER-SIDE 60 | by using bind method you can enable properties to be used in dynamic query generation. Client is allowed to use the properties only bound via bind method. 61 | if DTO properties are different from the entity properties then you have to specify it as the first argument of the bind method e.g. `bind("employeeName", Employee_.name)`. Otherwise, you can fell free to omit it e.g. `bind(Employee_.name)`. 62 | 63 | ![](.README_images/specification1.png) 64 | ``` 65 | final Specification specification = SpecificationBuilder.of(criteriaDTO) 66 | .bind("employeeName", Employee_.name) 67 | .bind("employeeSurname", Employee_.surname) 68 | .bind("employeeEmail", Employee_.email) 69 | .bind("employeeBirthDate", Employee_.birthDate) 70 | .bindJoin("phoneNumber", Employee_.phones, Phone_.number) 71 | .build(); 72 | 73 | var customerFromDB = employeeRepository.findOne(specification) 74 | .orElseThrow(() -> new NoSuchElementException()); 75 | ``` 76 | If your dto and entity share common names for properties you can simply define as follows: 77 | ``` 78 | final Specification specification = SpecificationBuilder.of(criteriaDTO) 79 | .bind(Employee_.name) 80 | .bind(Employee_.surname) 81 | .bind(Employee_.email) 82 | .bind(Employee_.birthDate) 83 | .bindJoin(Employee_.phones, Phone_.number) 84 | .build(); 85 | 86 | var customerFromDB = employeeRepository.findOne(specification) 87 | .orElseThrow(() -> new NoSuchElementException()); 88 | ``` 89 | For joins, you should use `bindJoin` instead e.g. `bindJoin("phoneNumber", Employee_.phones, Phone_.number)` or `bindJoin(Employee_.phones, Phone_.number)` 90 | You can add custom specifications by using `bindCustom` method 91 | #### PAGINATION 92 | For returning query results page by page you should pass sort information via `PageRequestDTO` instead of `CriteriaDTO` and then use PageRequestBuilder as follows: 93 | ```` 94 | final Specification specification = SpecificationBuilder.of(pageRequestDTO) 95 | .bind("employeeName", Employee_.name) 96 | .bind("employeeSurname", Employee_.surname) 97 | .bind("employeeEmail", Employee_.email) 98 | .bind("employeeBirthDate", Employee_.birthDate) 99 | .bindJoin("phoneNumber", Employee_.phones, Phone_.number) 100 | .build(); 101 | 102 | var pageRequest = PageRequestBuilder.of(pageRequestDTO) 103 | .bindSort("employeeName", Employee_.name) 104 | .bindSort("phoneNumber", Phone_.number) 105 | .build(); 106 | 107 | Page page = employeeRepository.findAll(specification, pageRequest); 108 | 109 | PageResultDTO pageResultDTO = PageResultDTO.from(page, EmployeeMapper.INSTANCE::toDTO); 110 | ```` 111 | If you don't want to use map struct library, you can write it explicitly as follows: 112 | ```` 113 | PageResultDTO pageResultDTO = PageResultDTO.from(page, e -> { 114 | EmployeeResponseDTO dto = new EmployeeResponseDTO(); 115 | dto.setName(e.getName()); 116 | dto.setSurname(e.getSurname()); 117 | dto.setEmail(e.getEmail()); 118 | return dto; 119 | }); 120 | 121 | ```` 122 | 123 | #### CLIENT-SIDE 124 | On the client side you should pass the property, its value, and operation that will be used in the query generation. 125 | Notice that some operators take no arguments (e.g. NULL, NOT_NULL, TRUE), some takes single, multiple values or range values as operands. 126 | So, you should follow the constraints of each operator described below: 127 | 128 | * `EQ, NOT_EQ, GT, GE, LT; LE` these operators take only one value as an argument: 129 | ```` 130 | { 131 | .. 132 | "operations": [ 133 | { 134 | "property": "name", 135 | "operator": "EQ", 136 | "value": "Alice" 137 | }, 138 | { 139 | "property": "age", 140 | "operator": "GE", 141 | "value": 18 142 | } 143 | ] 144 | } 145 | ```` 146 | * `IN, NOT_IN` these operators take multi-value as an argument: 147 | ```` 148 | { 149 | "operations": [ 150 | { 151 | "property": "customerId", 152 | "operator": "IN", 153 | "value": [ 154 | 1, 155 | 2, 156 | 3, 157 | 4, 158 | 5 159 | ] 160 | } 161 | ] 162 | } 163 | ```` 164 | * `BT` this operator takes range of values as an argument: 165 | ```` 166 | { 167 | "operations": [ 168 | { 169 | "property": "age", 170 | "operator": "BT", 171 | "value": { 172 | "low": 18, 173 | "high": 65 174 | } 175 | } 176 | ] 177 | } 178 | ```` 179 | * `NULL, NOT_NULL, TRUE, FALSE` these operators take no value as an argument: 180 | ```` 181 | { 182 | "operations": [ 183 | { 184 | "property": "phoneNumber", 185 | "operator": "NOT_NULL" 186 | } 187 | ] 188 | } 189 | ```` 190 | 191 | Sort order, requested page, and page size information can be passed as follows: 192 | ```` 193 | { 194 | .. 195 | "sortFields": [ 196 | { 197 | "property": "name", 198 | "direction": "ASC" 199 | }, 200 | { 201 | "property": "surname", 202 | "direction": "DESC" 203 | } 204 | ], 205 | "page": 0, 206 | "size": 10 207 | } 208 | ```` 209 | #### SAMPLE PROJECT 210 | There is a [sample project repository](https://github.com/ozayduman/spring-data-specification) that demonstrates usage of specificaiton-builder. 211 | #### HOW TO BUILD 212 | * Requires Java 14 213 | * Executing tests: `./mvn test` (test reports: [./build/reports/tests/test/index.html](./build/reports/tests/test/index.html), code coverage reports: [./build/reports/jacoco/test/html/index.html](./build/reports/jacoco/test/html/index.html)) 214 | * Creating jars: `./mvn clean install` (see [./build/libs](./build/libs)) 215 | #### HOW TO CONTRIBUTE 216 | [Fork](https://help.github.com/articles/fork-a-repo), and send a [pull request](https://help.github.com/articles/using-pull-requests) and keep your fork in [sync](https://help.github.com/articles/syncing-a-fork/) with the upstream repository. 217 | #### LICENSE 218 | Specification Builder is open source and can be found on GitHub. It is distributed under the Apache 2.0 License. 219 | #### [SAMPLE PROJECT](SAMPLE.md) 220 | -------------------------------------------------------------------------------- /SAMPLE.md: -------------------------------------------------------------------------------- 1 | # SPECIFICATION-BUILDER SAMPLE 2 | This section shows how to use [Specification Builder](https://github.com/ozayduman/spring-data-specification-builder) 3 | library to write type-safe (client-oriented dynamic) queries for search screens in spring data jpa. 4 | The full sample can be found in [spring-data-specification](https://github.com/ozayduman/spring-data-specification) repo. 5 | 6 | ``` 7 | 8 | com.github.ozayduman 9 | specification-builder 10 | 0.0.3 11 | 12 | ``` 13 | 14 | ``` 15 | @RestController 16 | @RequestMapping("/customer") 17 | @Transactional 18 | @RequiredArgsConstructor 19 | public class CustomerController { 20 | private final CustomerService customerService; 21 | 22 | @PostMapping("/query") 23 | public PageResultDTO query(@RequestBody PageRequestDTO pageRequestDTO){ 24 | return PageResultDTO.from(customerService.query(pageRequestDTO), CustomerMapper.INSTANCE::toDTO); 25 | } 26 | } 27 | ``` 28 | Then define as a Service class by injecting CustomerRepository class. In this class create a SpecificationBuilder and 29 | use bind method to allow whatever fields you want to be queryable. Also create a PageRequestBuilder and use bindSort method 30 | to allow which fields to be sortable as shown below: 31 | ``` 32 | @Service 33 | @RequiredArgsConstructor 34 | public class CustomerService { 35 | private final CustomerRepository customerRepository; 36 | 37 | public Page query(PageRequestDTO pageRequestDTO) { 38 | final Specification specification = createSpecification(pageRequestDTO); 39 | final PageRequest pageRequest = createPageRequest(pageRequestDTO); 40 | return customerRepository.findAll(specification, pageRequest); 41 | } 42 | 43 | private Specification createSpecification(PageRequestDTO pageRequestDTO) { 44 | return SpecificationBuilder.of(pageRequestDTO) 45 | .bind("name", Customer_.name) 46 | .bind("lastName", Customer_.surname) 47 | .bind("email", Customer_.email) 48 | .bindJoin("phoneNumber", Customer_.phones, Phone_.number) 49 | .build(); 50 | } 51 | 52 | private PageRequest createPageRequest(PageRequestDTO pageRequestDTO) { 53 | return PageRequestDTO.PageRequestBuilder.of(pageRequestDTO) 54 | .bindSort("name", Customer_.name) 55 | .bindSort("lastName", Customer_.surname) 56 | .bindSort("email", Customer_.email) 57 | .build(); 58 | } 59 | } 60 | ``` 61 | Finally, define a repository interface that extends PagingAndSortingReporistory and JpaSpecificationExecutor interfaces 62 | as follows: 63 | ```` 64 | public interface CustomerRepository extends PagingAndSortingRepository, JpaSpecificationExecutor { } 65 | ```` 66 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.4.4 9 | 10 | 11 | com.github.ozayduman 12 | specification-builder 13 | 0.0.6 14 | jar 15 | specification-builder 16 | Specification-Builder is a client-oriented dynamic search query library that supports joins among multiple tables in a strongly-type manner for Spring Projects. 17 | https://github.com/ozayduman/specification-builder 18 | 19 | 16 20 | 1.3.1.Final 21 | 22 | 23 | 24 | 25 | Apache License, 2.0 26 | https://opensource.org/licenses/Apache-2.0 27 | repo 28 | 29 | 30 | 31 | 32 | 33 | Ozay Duman 34 | ozay.duman@gmail.com 35 | Ozay Duman 36 | https://github.com/ozayduman 37 | 38 | 39 | 40 | https://github.com/ozayduman/specification-builder/tree/master 41 | 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-data-jpa 47 | 48 | 49 | org.projectlombok 50 | lombok 51 | 1.18.20 52 | true 53 | 54 | 55 | com.fasterxml.jackson.datatype 56 | jackson-datatype-jsr310 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-test 61 | test 62 | 63 | 64 | com.h2database 65 | h2 66 | test 67 | 68 | 69 | org.junit.jupiter 70 | junit-jupiter-engine 71 | test 72 | 73 | 74 | org.mapstruct 75 | mapstruct 76 | test 77 | ${org.mapstruct.version} 78 | 79 | 80 | org.mapstruct 81 | mapstruct-processor 82 | test 83 | ${org.mapstruct.version} 84 | 85 | 86 | 87 | 88 | 89 | ossrh 90 | https://oss.sonatype.org/content/repositories/snapshots 91 | 92 | 93 | ossrh 94 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 95 | 96 | 97 | 98 | 99 | 100 | 101 | maven-clean-plugin 102 | 3.1.0 103 | 104 | 105 | 106 | org.apache.maven.plugins 107 | maven-javadoc-plugin 108 | 109 | 110 | attach-javadocs 111 | 112 | jar 113 | 114 | 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-source-plugin 121 | 122 | 123 | attach-sources 124 | 125 | jar-no-fork 126 | 127 | 128 | 129 | 130 | 131 | 135 | 136 | org.apache.maven.plugins 137 | maven-compiler-plugin 138 | 3.8.1 139 | 140 | ${java.version} 141 | ${java.version} 142 | true 143 | 144 | 145 | org.projectlombok 146 | lombok 147 | 1.18.20 148 | 149 | 150 | org.hibernate 151 | hibernate-jpamodelgen 152 | 5.4.5.Final 153 | 154 | 155 | 156 | 157 | 158 | 159 | org.apache.maven.plugins 160 | maven-gpg-plugin 161 | 1.6 162 | 163 | 164 | sign-artifacts 165 | verify 166 | 167 | sign 168 | 169 | 170 | 171 | 172 | 173 | 174 | org.sonatype.plugins 175 | nexus-staging-maven-plugin 176 | 1.6.7 177 | true 178 | 179 | ossrh 180 | https://oss.sonatype.org/ 181 | true 182 | 183 | 184 | 185 | 186 | maven-surefire-plugin 187 | 2.22.1 188 | 189 | 190 | maven-jar-plugin 191 | 3.0.2 192 | 193 | 194 | maven-install-plugin 195 | 2.5.2 196 | 197 | 198 | maven-deploy-plugin 199 | 2.8.2 200 | 201 | 202 | 203 | maven-site-plugin 204 | 3.7.1 205 | 206 | 207 | maven-project-info-reports-plugin 208 | 3.0.0 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/Joinable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder; 34 | 35 | import javax.persistence.metamodel.Attribute; 36 | import java.util.Optional; 37 | 38 | /** 39 | * Represents the join behavior, each {@code #attributes} chain represents a join 40 | */ 41 | public interface Joinable { 42 | 43 | 44 | /** 45 | * @return join item attributes as an array 46 | */ 47 | Optional[]> attributes(); 48 | 49 | /** 50 | * creates the non joinable type 51 | * @return {@link NoJoin} 52 | */ 53 | static Joinable non(){ 54 | return new NoJoin(); 55 | } 56 | 57 | /** 58 | * creates a joinable type 59 | * @param joinAttribute represents each item in the join as an array 60 | * @return {@link AttributeJoin} 61 | */ 62 | static Joinable join(Attribute[] joinAttribute){ 63 | return new AttributeJoin(joinAttribute); 64 | } 65 | 66 | /** 67 | * Represents a non joinable type. When there is no need for a join, this class is used 68 | */ 69 | class NoJoin implements Joinable{ 70 | @Override 71 | public Optional[]> attributes() { 72 | return Optional.empty(); 73 | } 74 | } 75 | 76 | /** 77 | * Represents joinable type holding the join chain as {@code #joinPluralAttribute}. 78 | */ 79 | class AttributeJoin implements Joinable{ 80 | private final Attribute[] joinAttribute; 81 | 82 | /** 83 | * @param joinAttribute creates {@code PluralAttributeJoin} with {@code #joinPluralAttribute} 84 | */ 85 | public AttributeJoin(Attribute[] joinAttribute) { 86 | this.joinAttribute = joinAttribute; 87 | } 88 | 89 | @Override 90 | public Optional[]> attributes() { 91 | return Optional.of(joinAttribute); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/SpecificationMappings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder; 34 | 35 | import com.fasterxml.jackson.databind.ObjectMapper; 36 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 37 | import com.github.ozayduman.specificationbuilder.dto.CriteriaDTO; 38 | import com.github.ozayduman.specificationbuilder.dto.PageRequestDTO; 39 | import com.github.ozayduman.specificationbuilder.dto.operation.AbstractOperation; 40 | import org.springframework.data.jpa.domain.Specification; 41 | 42 | import javax.persistence.criteria.*; 43 | import javax.persistence.metamodel.Attribute; 44 | import javax.persistence.metamodel.Bindable; 45 | import javax.persistence.metamodel.PluralAttribute; 46 | import javax.persistence.metamodel.SingularAttribute; 47 | import java.util.*; 48 | 49 | 50 | /** 51 | * This class holds dto entity mappings to generate dynamic queries whose criteria supplied on the client-side 52 | * 53 | * @param the root entity type supplied to this mappings. 54 | */ 55 | public class SpecificationMappings { 56 | private final CriteriaDTO criteriaDTO; 57 | private final Map>> dtoEntityMapping; 58 | private final Map dtoJoinMappings; 59 | private final JoinGraph joinGraph; 60 | 61 | private SpecificationMappings(CriteriaDTO criteriaDTO, Map>> dtoEntityMapping, Map dtoJoinMappings) { 62 | this.criteriaDTO = criteriaDTO; 63 | this.dtoEntityMapping = dtoEntityMapping; 64 | this.dtoJoinMappings = dtoJoinMappings; 65 | this.joinGraph = new JoinGraph(); 66 | } 67 | 68 | /** 69 | * @return {@code Specification} 70 | */ 71 | private Specification createSpecification() { 72 | return (Root root, CriteriaQuery cQ, CriteriaBuilder cb) -> { 73 | List predicates = new ArrayList<>() {{ 74 | addAll(createOperationPredicates(root, cQ, cb, criteriaDTO)); 75 | }}; 76 | return predicates.isEmpty() ? cb.conjunction() : cb.and(predicates.toArray(new Predicate[predicates.size()])); 77 | }; 78 | } 79 | 80 | private List createOperationPredicates(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder, 81 | final CriteriaDTO criteriaDTO) { 82 | List predicates = new ArrayList<>(); 83 | if (criteriaDTO != null && criteriaDTO.getOperations() != null) { 84 | criteriaDTO.getOperations().forEach(operation -> { 85 | Comparable[] values = operation.getOperands(); 86 | var operator = operation.getOperator().getSpecificationOperator(); 87 | final var predicate = createOperandPredicate(root, criteriaBuilder, operator, operation.getProperty(), values); 88 | predicates.add(predicate); 89 | }); 90 | } 91 | return predicates; 92 | } 93 | 94 | /** 95 | * @param root represents JPA root entity 96 | * @param criteriaBuilder represents jPA criteriaBuilder 97 | * @param operator represents {@link SpecificationOperator} 98 | * @param dtoProperty represents the property of DTO 99 | * @param value represents the corresponding value of {@code dtoProperty} 100 | * @return {@code Predicate} 101 | */ 102 | private Predicate createOperandPredicate(Root root, CriteriaBuilder criteriaBuilder, SpecificationOperator operator, String dtoProperty, Comparable... value) { 103 | final SingularAttribute attribute = dtoEntityMapping.get(dtoProperty); 104 | Objects.requireNonNull(attribute, () -> String.format("DTO property named : %s could not be found in eq map ", dtoProperty)); 105 | final var from = joinGraph.from(root, dtoJoinMappings.getOrDefault(dtoProperty, Joinable.non()).attributes()); 106 | final Comparable[] convertedValues = getConvertedValue(attribute.getJavaType(), value); 107 | return operator.apply(from, criteriaBuilder, attribute, convertedValues); 108 | } 109 | 110 | /** 111 | * Deserializes then given {@code value} array back to real object using {@code javaType} 112 | * 113 | * @param javaType real type of the object 114 | * @param value serialized value of the real object 115 | * @return {@code Comparable[]} 116 | */ 117 | private Comparable[] getConvertedValue(Class javaType, Object... value) { 118 | return Arrays.stream(value).map(val -> ObjectMapper_.INSTANCE.convert(val, javaType)).toArray(Comparable[]::new); 119 | } 120 | 121 | /** 122 | * Singleton type used to convert json to object and vise-versa 123 | */ 124 | private enum ObjectMapper_ { 125 | INSTANCE; 126 | private final ObjectMapper objectMapper; 127 | 128 | ObjectMapper_() { 129 | objectMapper = new ObjectMapper(); 130 | objectMapper.registerModule(new JavaTimeModule()); 131 | } 132 | 133 | /** 134 | * @param fromValue json object to be deserialized to the real object type 135 | * @param toJavaType the real type to be converted 136 | * @return 137 | */ 138 | public Object convert(Object fromValue, Class toJavaType) { 139 | return objectMapper.convertValue(fromValue, toJavaType); 140 | } 141 | } 142 | 143 | /** 144 | * Represents the JoinGraph 145 | */ 146 | public static class JoinGraph { 147 | private final Map, JoinNode> mapOfSets = new HashMap<>(); 148 | 149 | /** 150 | * Serves Acts as a Join Cache role by reusing the Join instances among different Specification instances 151 | * 152 | * @param root 153 | * @param joinAttributes represents the entities between the root entity and the last entity in the hierarchy of the {@code JoinGraph} 154 | * @return if joinAttributes present then join {@code From}, otherwise root {@code Root} 155 | */ 156 | private From from(Root root, Optional[]> joinAttributes) { 157 | if (joinAttributes.isPresent()) { 158 | From join = root; 159 | Map, JoinNode> currentMapOfSets = mapOfSets; 160 | for (Attribute attribute : joinAttributes.get()) { 161 | From finalJoin = join; 162 | var joinNode = currentMapOfSets.computeIfAbsent(attribute, a -> JoinNode.of(attribute, finalJoin)); 163 | join = joinNode.getJoin(); 164 | currentMapOfSets = joinNode.joinNodes; 165 | } 166 | return join; 167 | } else { 168 | return root; 169 | } 170 | } 171 | 172 | /** 173 | * Represents the {@code JoinGraph}'s nodes 174 | */ 175 | static class JoinNode { 176 | public final Attribute attribute; 177 | public final From join; 178 | public final Map, JoinNode> joinNodes = new HashMap<>(); 179 | 180 | private JoinNode(From from, Attribute attribute) { 181 | this.attribute = attribute; 182 | this.join = from.join(attribute.getName()); 183 | } 184 | 185 | 186 | /** 187 | * Static Factory Method to create {@code JoinNode} 188 | * 189 | * @param attribute represents the entity for this join 190 | * @param from represents the preceding join 191 | * @return {@code JoinNode} 192 | */ 193 | public static JoinNode of(Attribute attribute, From from) { 194 | return new JoinNode(from, attribute); 195 | } 196 | 197 | public From getJoin() { 198 | return join; 199 | } 200 | } 201 | } 202 | 203 | /** 204 | * SpecificationBuilder creates {@code Specification} using the supplied {@code CriteriaDTO} or {@code PageRequestDTO} 205 | * and by using bind method you can enable properties to be used in dynamic query generation. 206 | * 207 | * @param the entity type supplied to this builder. 208 | *

Sample usage:

209 | *
210 |      *            {@code final Specification specification = SpecificationBuilder.of(pageRequestDTO)
211 |      *                            .bind("employeeName", Employee_.name)
212 |      *                            .bind("employeeSurname", Employee_.surname)
213 |      *                            .bind("employeeEmail", Employee_.email)
214 |      *                            .bind("employeeBirthDate", Employee_.birthDate)
215 |      *                            .bindJoin("phoneNumber", Employee_.phones, Phone_.number)
216 |      *                            .build();
217 |      *
218 |      *                    var pageRequest = PageRequestBuilder.of(pageRequestDTO)
219 |      *                            .bindSort("employeeName", Employee_.name)
220 |      *                            .bindSort("phoneNumber", Phone_.number)
221 |      *                            .build();
222 |      *
223 |      *                    Page page = employeeRepository.findAll(specification, pageRequest);}
224 |      *            
225 | */ 226 | public static class SpecificationBuilder { 227 | private final CriteriaDTO criteriaDTO; 228 | private final Map>> dtoEntityMapping = new HashMap<>(); 229 | private final Map dtoJoinMappings = new HashMap<>(); 230 | 231 | private SpecificationBuilder(CriteriaDTO criteriaDTO) { 232 | this.criteriaDTO = criteriaDTO; 233 | } 234 | 235 | /** 236 | * Static Factory method that creates {@code SpecificationBuilder} with given {@code CriteriaDTO} 237 | * or {@code PageRequestDTO} object 238 | * 239 | * @param criteriaDTO or {@link PageRequestDTO} is a DTO from client-side holding criteria information 240 | * @param the entity type supplied to this builder. 241 | * @return a new {@code SpecificationBuilder} 242 | */ 243 | public static SpecificationBuilder of(CriteriaDTO criteriaDTO) { 244 | Objects.requireNonNull(criteriaDTO, "a criteria DTO must not be supplied"); 245 | if (criteriaDTO.getOperations() != null) { 246 | criteriaDTO.getOperations().forEach(AbstractOperation::validate); 247 | } 248 | return new SpecificationBuilder<>(criteriaDTO); 249 | } 250 | 251 | /** 252 | * @param entityProperty represents the matching the server entity property 253 | * @param the type of the represented entity property 254 | * @return currently (this) running {@code SpecificationBuilder} 255 | */ 256 | public > SpecificationBuilder bind(SingularAttribute entityProperty) { 257 | bind(entityProperty.getName(), entityProperty); 258 | return this; 259 | } 260 | 261 | /** 262 | * @param dtoProperty represents the client property name 263 | * @param entityProperty represents the matching the server entity property 264 | * @param the type of the represented entity property 265 | * @return currently (this) running {@code SpecificationBuilder} 266 | */ 267 | public > SpecificationBuilder bind(String dtoProperty, SingularAttribute entityProperty) { 268 | dtoEntityMapping.put(dtoProperty, entityProperty); 269 | dtoJoinMappings.put(dtoProperty, Joinable.non()); 270 | return this; 271 | } 272 | 273 | /** 274 | * @param pluralAttribute represents the root entity 275 | * @param entityProperty represents the matching the server entity property 276 | * @param the type of the represented entity property 277 | * @param represents the entity (sub entity) contained by the root entity 278 | * @param the type of the represented last leaf entity property 279 | * @return currently (this) running {@code SpecificationBuilder} 280 | */ 281 | public > SpecificationBuilder bindJoin(PluralAttribute pluralAttribute, 282 | SingularAttribute entityProperty) { 283 | mapJoin(entityProperty.getName(), entityProperty, pluralAttribute); 284 | return this; 285 | } 286 | 287 | /** 288 | * @param singularAttribute represents the root entity 289 | * @param entityProperty represents the matching the server entity property 290 | * @param the type of the represented entity property 291 | * @param represents the entity (sub entity) contained by the root entity 292 | * @param the type of the represented last leaf entity property 293 | * @return currently (this) running {@code SpecificationBuilder} 294 | */ 295 | public > SpecificationBuilder bindJoin(SingularAttribute singularAttribute, 296 | SingularAttribute entityProperty) { 297 | mapJoin(entityProperty.getName(), entityProperty, singularAttribute); 298 | return this; 299 | } 300 | 301 | /** 302 | * @param dtoProperty represents the client property name 303 | * @param pluralAttribute represents the root entity 304 | * @param entityProperty represents the matching the server entity property 305 | * @param the type of the represented entity property 306 | * @param represents the entity (sub entity) contained by the root entity 307 | * @param the type of the represented last leaf entity property 308 | * @return currently (this) running {@code SpecificationBuilder} 309 | */ 310 | public > SpecificationBuilder bindJoin(String dtoProperty, 311 | PluralAttribute pluralAttribute, 312 | SingularAttribute entityProperty) { 313 | mapJoin(dtoProperty, entityProperty, pluralAttribute); 314 | return this; 315 | } 316 | 317 | /** 318 | * @param dtoProperty represents the client property name 319 | * @param singularAttribute0 represents the root entity 320 | * @param entityProperty represents the matching the server entity property 321 | * @param the type of the represented entity property 322 | * @param represents the entity (sub entity) contained by the root entity 323 | * @param the type of the represented last leaf entity property 324 | * @return currently (this) running {@code SpecificationBuilder} 325 | */ 326 | public > SpecificationBuilder bindJoin(String dtoProperty, 327 | SingularAttribute singularAttribute0, 328 | SingularAttribute entityProperty) { 329 | mapJoin(dtoProperty, entityProperty, singularAttribute0); 330 | return this; 331 | } 332 | 333 | 334 | /** 335 | * @param pluralAttribute0 represents the root entity 336 | * @param pluralAttribute1 represents the sub entity under {@code #pluralAttribute0} 337 | * @param entityProperty represents the matching the server entity property 338 | * @param the type of the represented entity property 339 | * @param represents the entity (sub entity) contained by the root entity 340 | * @param represents the entity under type {@code B} 341 | * @param the type of the represented last leaf entity property 342 | * @return currently (this) running {@code SpecificationBuilder} 343 | */ 344 | public > SpecificationBuilder bindJoin(PluralAttribute pluralAttribute0, 345 | PluralAttribute pluralAttribute1, 346 | SingularAttribute entityProperty) { 347 | mapJoin(entityProperty.getName(), entityProperty, pluralAttribute0, pluralAttribute1); 348 | return this; 349 | } 350 | 351 | /** 352 | * @param dtoProperty represents the client property name 353 | * @param pluralAttribute0 represents the root entity 354 | * @param pluralAttribute1 represents the sub entity under {@code #pluralAttribute0} 355 | * @param entityProperty represents the matching the server entity property 356 | * @param the type of the represented entity property 357 | * @param represents the entity (sub entity) contained by the root entity 358 | * @param represents the entity under type {@code B} 359 | * @param the type of the represented last leaf entity property 360 | * @return currently (this) running {@code SpecificationBuilder} 361 | */ 362 | public > SpecificationBuilder bindJoin(String dtoProperty, 363 | PluralAttribute pluralAttribute0, 364 | PluralAttribute pluralAttribute1, 365 | SingularAttribute entityProperty) { 366 | mapJoin(dtoProperty, entityProperty, pluralAttribute0, pluralAttribute1); 367 | return this; 368 | } 369 | 370 | /** 371 | * @param pluralAttribute0 represents the root entity 372 | * @param pluralAttribute1 represents the sub entity under {@code #pluralAttribute0} 373 | * @param pluralAttribute2 represents the sub entity under {@code #pluralAttribute1} 374 | * @param entityProperty represents the matching the server entity property 375 | * @param the type of the represented entity property 376 | * @param represents the entity (sub entity) contained by the root entity 377 | * @param represents the entity under type {@code B} 378 | * @param represents the entity under type {@code C} 379 | * @param the type of the represented last leaf entity property 380 | * @return currently (this) running {@code SpecificationBuilder} 381 | */ 382 | public > SpecificationBuilder bindJoin(PluralAttribute pluralAttribute0, 383 | PluralAttribute pluralAttribute1, 384 | PluralAttribute pluralAttribute2, 385 | SingularAttribute entityProperty) { 386 | mapJoin(entityProperty.getName(), entityProperty, pluralAttribute0, pluralAttribute1, pluralAttribute2); 387 | return this; 388 | } 389 | 390 | /** 391 | * @param dtoProperty represents the client property name 392 | * @param pluralAttribute0 represents the root entity 393 | * @param pluralAttribute1 represents the sub entity under {@code #pluralAttribute0} 394 | * @param pluralAttribute2 represents the sub entity under {@code #pluralAttribute1} 395 | * @param entityProperty represents the matching the server entity property 396 | * @param the type of the represented entity property 397 | * @param represents the entity (sub entity) contained by the root entity 398 | * @param represents the entity under type {@code B} 399 | * @param represents the entity under type {@code C} 400 | * @param the type of the represented last leaf entity property 401 | * @return currently (this) running {@code SpecificationBuilder} 402 | */ 403 | public > SpecificationBuilder bindJoin(String dtoProperty, 404 | PluralAttribute pluralAttribute0, 405 | PluralAttribute pluralAttribute1, 406 | PluralAttribute pluralAttribute2, 407 | SingularAttribute entityProperty) { 408 | mapJoin(dtoProperty, entityProperty, pluralAttribute0, pluralAttribute1, pluralAttribute2); 409 | return this; 410 | } 411 | 412 | /** 413 | * @param pluralAttribute0 represents the root entity 414 | * @param pluralAttribute1 represents the sub entity under {@code #pluralAttribute0} 415 | * @param pluralAttribute2 represents the sub entity under {@code #pluralAttribute1} 416 | * @param pluralAttribute3 represents the sub entity under {@code #pluralAttribute2} 417 | * @param entityProperty represents the matching the server entity property 418 | * @param the type of the represented entity property 419 | * @param represents the entity (sub entity) contained by the root entity 420 | * @param represents the entity under type {@code B} 421 | * @param represents the entity under type {@code C} 422 | * @param represents the entity under type {@code D} 423 | * @param The type of the represented last leaf entity property 424 | * @return currently (this) running {@code SpecificationBuilder} 425 | */ 426 | public > SpecificationBuilder bindJoin(PluralAttribute pluralAttribute0, 427 | PluralAttribute pluralAttribute1, 428 | PluralAttribute pluralAttribute2, 429 | PluralAttribute pluralAttribute3, 430 | SingularAttribute entityProperty) { 431 | mapJoin(entityProperty.getName(), entityProperty, pluralAttribute0, pluralAttribute1, pluralAttribute2, pluralAttribute3); 432 | return this; 433 | } 434 | 435 | /** 436 | * @param dtoProperty represents the client property name 437 | * @param pluralAttribute0 represents the root entity 438 | * @param pluralAttribute1 represents the sub entity under {@code #pluralAttribute0} 439 | * @param pluralAttribute2 represents the sub entity under {@code #pluralAttribute1} 440 | * @param pluralAttribute3 represents the sub entity under {@code #pluralAttribute2} 441 | * @param entityProperty represents the matching the server entity property 442 | * @param the type of the represented entity property 443 | * @param represents the entity (sub entity) contained by the root entity 444 | * @param represents the entity under type {@code B} 445 | * @param represents the entity under type {@code C} 446 | * @param represents the entity under type {@code D} 447 | * @param The type of the represented last leaf entity property 448 | * @return currently (this) running {@code SpecificationBuilder} 449 | */ 450 | public > SpecificationBuilder bindJoin(String dtoProperty, 451 | PluralAttribute pluralAttribute0, 452 | PluralAttribute pluralAttribute1, 453 | PluralAttribute pluralAttribute2, 454 | PluralAttribute pluralAttribute3, 455 | SingularAttribute entityProperty) { 456 | mapJoin(dtoProperty, entityProperty, pluralAttribute0, pluralAttribute1, pluralAttribute2, pluralAttribute3); 457 | return this; 458 | } 459 | 460 | /** 461 | * @param singularAttribute represents the matching the server entity property 462 | * @param joinAttributes represents the entities between the root entity and the last entity in the hierarchy 463 | */ 464 | private void mapJoin(SingularAttribute> singularAttribute, PluralAttribute... joinAttributes) { 465 | mapJoin(singularAttribute.getName(), singularAttribute, joinAttributes); 466 | } 467 | 468 | 469 | /** 470 | * @param dtoProperty represents the client property name 471 | * @param singularAttribute represents the matching the server entity property 472 | * @param joinAttributes holds the entities between the root entity and the last entity in the hierarchy 473 | */ 474 | /* private void mapJoin(String dtoProperty, SingularAttribute> singularAttribute, PluralAttribute... joinAttributes) { 475 | dtoEntityMapping.put(dtoProperty, singularAttribute); 476 | dtoJoinMappings.put(dtoProperty, Joinable.join(joinAttributes)); 477 | }*/ 478 | 479 | private void mapJoin(String dtoProperty, SingularAttribute> singularAttribute, Attribute... joinAttributes) { 480 | dtoEntityMapping.put(dtoProperty, singularAttribute); 481 | dtoJoinMappings.put(dtoProperty, Joinable.join(joinAttributes)); 482 | } 483 | 484 | /** 485 | * builds a {@code Specification} from this {@code SpecificationBuilder} 486 | * 487 | * @return {@code Specification} 488 | */ 489 | public Specification build() { 490 | final SpecificationMappings specificationMapper = new SpecificationMappings<>(criteriaDTO, dtoEntityMapping, dtoJoinMappings); 491 | return specificationMapper.createSpecification(); 492 | } 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/SpecificationOperator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder; 34 | 35 | import javax.persistence.criteria.CriteriaBuilder; 36 | import javax.persistence.criteria.From; 37 | import javax.persistence.criteria.Predicate; 38 | import javax.persistence.metamodel.SingularAttribute; 39 | import java.net.URL; 40 | 41 | /** 42 | * A functional interface represents Query Operators that will be used to build a Specification. 43 | */ 44 | @FunctionalInterface 45 | public interface SpecificationOperator { 46 | 47 | /** 48 | * @param from Represents {@code javax.persistence.Criteria.Root} or {@code javax.persistence.Criteria.Join} 49 | * @param cb Represents {@code CriteriaBuilder} 50 | * @param attribute Represents entity as a {@code SingularAttribute} 51 | * @param values Represents operation's values 52 | * @return {@code Predicate} 53 | */ 54 | Predicate apply(From from, CriteriaBuilder cb, SingularAttribute attribute, Comparable[] values); 55 | 56 | /** 57 | * Represents equality function 58 | * @return {@link SpecificationOperator} 59 | */ 60 | static SpecificationOperator eq(){ 61 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 62 | cb.equal(from.get(attribute.getName()), values[0]); 63 | } 64 | 65 | /** 66 | * Represents not equal function 67 | * @return {@link SpecificationOperator} 68 | */ 69 | static SpecificationOperator notEq(){ 70 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 71 | cb.notEqual(from.get(attribute.getName()), values[0]); 72 | } 73 | 74 | /** 75 | * Represents between function 76 | * @return {@link SpecificationOperator} 77 | */ 78 | static SpecificationOperator bt(){ 79 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 80 | cb.between(from.get(attribute.getName()), values[0], values[1]); 81 | } 82 | 83 | /** 84 | * Represents greater than function 85 | * @return {@link SpecificationOperator} 86 | */ 87 | static SpecificationOperator gt(){ 88 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 89 | cb.greaterThan(from.get(attribute.getName()), values[0]); 90 | } 91 | 92 | /** 93 | * Represents greater than or equal to function 94 | * @return {@link SpecificationOperator} 95 | */ 96 | static SpecificationOperator ge(){ 97 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 98 | cb.greaterThanOrEqualTo(from.get(attribute.getName()), values[0]); 99 | } 100 | 101 | /** 102 | * Represents less than function 103 | * @return {@link SpecificationOperator} 104 | */ 105 | static SpecificationOperator lt(){ 106 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 107 | cb.lessThan(from.get(attribute.getName()), values[0]); 108 | } 109 | 110 | /** 111 | * Represents less than or equal to function 112 | * @return {@link SpecificationOperator} 113 | */ 114 | static SpecificationOperator le(){ 115 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 116 | cb.lessThanOrEqualTo(from.get(attribute.getName()), values[0]); 117 | } 118 | 119 | /** 120 | * Represents in function 121 | * @return {@link SpecificationOperator} 122 | */ 123 | static SpecificationOperator in(){ 124 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 125 | from.get(attribute.getName()).in(values); 126 | } 127 | 128 | /** 129 | * Represents not in function 130 | * @return {@link SpecificationOperator} 131 | */ 132 | static SpecificationOperator notIn(){ 133 | return not(in()); 134 | } 135 | 136 | /** 137 | * Represents null function 138 | * @return {@link SpecificationOperator} 139 | */ 140 | static SpecificationOperator isNull(){ 141 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 142 | cb.isNull(from.get(attribute.getName())); 143 | } 144 | 145 | /** 146 | * Represents not null function 147 | * @return {@link SpecificationOperator} 148 | */ 149 | static SpecificationOperator isNotNull(){ 150 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 151 | cb.isNotNull(from.get(attribute.getName())); 152 | } 153 | 154 | /** 155 | * Represents is true function 156 | * @return {@link SpecificationOperator} 157 | */ 158 | static SpecificationOperator isTrue(){ 159 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 160 | cb.isTrue(from.get(attribute.getName())); 161 | } 162 | 163 | /** 164 | * Represents is false function 165 | * @return {@link SpecificationOperator} 166 | */ 167 | static SpecificationOperator isFalse(){ 168 | return (From from, CriteriaBuilder cb,SingularAttribute attribute, Comparable[] values) -> 169 | cb.isFalse(from.get(attribute.getName())); 170 | } 171 | 172 | /** 173 | * Represents like function 174 | * @return {@link SpecificationOperator} 175 | */ 176 | static SpecificationOperator like(){ 177 | return (from, cb, attribute, values) -> cb.like(from.get(attribute.getName()), String.format("%%%s%%", values[0])); 178 | } 179 | 180 | /** 181 | * Represents not like function 182 | * @return {@link SpecificationOperator} 183 | */ 184 | static SpecificationOperator notLike(){ 185 | return (from, cb, attribute, values) -> cb.notLike(from.get(attribute.getName()), String.format("%%%s%%", values[0])); 186 | } 187 | 188 | /** Represents a Higher Order Function that inverts a given {@code SpecificationOperator} 189 | * @param operator is a Specification operator 190 | * @return {@link SpecificationOperator} 191 | */ 192 | static SpecificationOperator not(SpecificationOperator operator){ 193 | return operator == null 194 | ? (from, cb, attribute, values) -> null 195 | : (from, cb, attribute, values) -> cb.not(operator.apply(from, cb, attribute, values)); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/CriteriaDTO.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto; 34 | 35 | import com.github.ozayduman.specificationbuilder.dto.operation.AbstractOperation; 36 | import lombok.Getter; 37 | import lombok.NoArgsConstructor; 38 | import lombok.Setter; 39 | 40 | import java.util.List; 41 | import java.util.Map; 42 | 43 | /** 44 | * Represents dto type that holding filtering operations put by the query (search) screens 45 | * on the client-side and transferred to server-side. 46 | * This dto acts like a container role for {@code operations} dto. 47 | */ 48 | @Getter 49 | @Setter 50 | @NoArgsConstructor 51 | public class CriteriaDTO { 52 | private Map extras; 53 | private List operations; 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/Operator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto; 34 | 35 | import com.github.ozayduman.specificationbuilder.SpecificationOperator; 36 | 37 | import static com.github.ozayduman.specificationbuilder.SpecificationOperator.*; 38 | 39 | /** 40 | * This enum represents the Operators that can be passed by client-side for dynamic query generations. 41 | * This type has a direct reference to {@code specificationOperator} to simply finding corresponding function. 42 | */ 43 | public enum Operator { 44 | /** 45 | * Represents equal operator 46 | */ 47 | EQ(eq()), 48 | /** 49 | * Represents not equal operator 50 | */ 51 | NOT_EQ(notEq()), 52 | /** 53 | * Represents greater than operator 54 | */ 55 | GT(gt()), 56 | /** 57 | * Represents greater than or equal to operator 58 | */ 59 | GE(ge()), 60 | /** 61 | * Represents less than operator 62 | */ 63 | LT(lt()), 64 | /** 65 | * Represents less than or equal to operator 66 | */ 67 | LE(le()), 68 | /** 69 | * Represents between operator 70 | */ 71 | BT(bt()), 72 | /** 73 | * Represents in operator 74 | */ 75 | IN(in()), 76 | /** 77 | * Represents not in operator 78 | */ 79 | NOT_IN(notIn()), 80 | /** 81 | * Represents is null operator 82 | */ 83 | NULL(isNull()), 84 | /** 85 | * Represents is not null operator 86 | */ 87 | NOT_NULL(isNotNull()), 88 | /** 89 | * Represents is true operator 90 | */ 91 | TRUE(isTrue()), 92 | /** 93 | * Represents is false operator 94 | */ 95 | FALSE(isFalse()), 96 | /** 97 | * Represents is like operator 98 | */ 99 | LIKE(like()), 100 | /** 101 | * Represents is not like operator 102 | */ 103 | NOT_LIKE(notLike()); 104 | private SpecificationOperator specificationOperator; 105 | 106 | Operator(SpecificationOperator specificationOperator) { 107 | this.specificationOperator = specificationOperator; 108 | } 109 | 110 | /** 111 | * @return gets the corresponding {@link SpecificationOperator} 112 | */ 113 | public SpecificationOperator getSpecificationOperator() { 114 | return specificationOperator; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/PageRequestDTO.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto; 34 | 35 | import lombok.*; 36 | import org.springframework.data.domain.PageRequest; 37 | import org.springframework.data.domain.Sort; 38 | 39 | import javax.persistence.metamodel.SingularAttribute; 40 | import java.util.Arrays; 41 | import java.util.HashMap; 42 | import java.util.Map; 43 | import java.util.Objects; 44 | import java.util.function.Function; 45 | 46 | import static java.util.stream.Collectors.toList; 47 | 48 | /** 49 | * To request query results page by page, client should pass this {@code PageRequestDTO} type. 50 | *

This type holds current {@code page}, page {@code size} and also sort fields {@code sortFields}

51 | *

Note that default page size is 20 and if needed client can override this value by putting desired value in {@code size} property

52 | */ 53 | @Getter 54 | @Setter 55 | @NoArgsConstructor 56 | @AllArgsConstructor 57 | public class PageRequestDTO extends CriteriaDTO { 58 | private static final int DEFAULT_PAGE_SIZE = 20; 59 | private SortDTO[] sortFields; 60 | private int page, size; 61 | 62 | /** 63 | * @param sort to apply ordering 64 | * @return {@link PageRequest} 65 | */ 66 | private PageRequest createPageRequest(Sort sort){ 67 | return PageRequest.of(page, 68 | size > 1 ? size : DEFAULT_PAGE_SIZE, 69 | sortFields == null? Sort.unsorted() : sort); 70 | } 71 | 72 | /** 73 | * This dto is used to indicate sort information per client property. 74 | * Default {@code direction} for sorting is ASC. This can be overridden by passing direction explicitly. 75 | */ 76 | @Data 77 | @AllArgsConstructor 78 | public static class SortDTO{ 79 | private String property; 80 | private Direction direction; 81 | 82 | /** 83 | * Default constructor to create a {@code SortDTO}. Default sort direction is ASC. 84 | */ 85 | public SortDTO() { 86 | this.direction = Direction.ASC; 87 | } 88 | 89 | /** 90 | * @param property Represents SortDTO's {@code property} 91 | */ 92 | public SortDTO(String property) { 93 | this(); 94 | this.property = property; 95 | } 96 | 97 | /** 98 | * Default sort direction is ASC. This can be overridden by passing explicitly 99 | */ 100 | public enum Direction{ 101 | /** 102 | * Represents Ascending direction 103 | */ 104 | ASC, 105 | /** 106 | * Represents Descending direction 107 | */ 108 | DESC; 109 | 110 | Direction() { 111 | } 112 | 113 | /** 114 | * @return Converts {@code Direction} to {@code Sort.Direction} 115 | */ 116 | public Sort.Direction toSortDirection(){ 117 | return Sort.Direction.fromString(name()); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * This builder type is used to create PageRequest from {@code pageRequestDTO}. 124 | *

sorting fields from the client-side can be bound as follows:

125 | *
{@code var pageRequest = PageRequestBuilder.of(pageRequestDTO)
126 |      *                 .bindSort("property", Employee_.name)
127 |      *                 .bindSort("phoneNumber", Phone_.number)
128 |      *                 .build();}
129 |      * 
130 | * 131 | */ 132 | public static class PageRequestBuilder { 133 | private PageRequestDTO pageRequestDTO; 134 | private Map> dtoEntityMapping = new HashMap<>(); 135 | 136 | private PageRequestBuilder(PageRequestDTO pageRequestDTO) { 137 | this.pageRequestDTO = pageRequestDTO; 138 | } 139 | 140 | /** 141 | * Creates a {@code PageRequestBuilder} from {@code pageRequestDTO} 142 | * @param pageRequestDTO from client-side 143 | * @return {@code PageRequestBuilder} 144 | */ 145 | public static PageRequestBuilder of(PageRequestDTO pageRequestDTO){ 146 | return new PageRequestBuilder(pageRequestDTO); 147 | } 148 | 149 | /** 150 | * This method can be used to bind client and server-side does not share a common naming for the property. 151 | * 152 | * @param attribute entity property on the server-side 153 | * @param typed of the {@code value} 154 | * @return {@code PageRequestBuilder} 155 | */ 156 | public > PageRequestBuilder bindSort(SingularAttribute attribute) { 157 | return bindSort(attribute.getName(), attribute); 158 | } 159 | 160 | /** 161 | * This method can be used to bind client and server-side does not share a common naming for the property. 162 | * 163 | * @param property from client-side 164 | * @param value entity property on the server-side 165 | * @param typed of the {@code value} 166 | * @return {@code PageRequestBuilder} 167 | */ 168 | public > PageRequestBuilder bindSort(String property, SingularAttribute value) { 169 | dtoEntityMapping.putIfAbsent(property, value); 170 | return this; 171 | } 172 | 173 | /** 174 | * @return {@code PageRequest} 175 | */ 176 | public PageRequest build() { 177 | var orders = mapSortFields(); 178 | return pageRequestDTO.createPageRequest(orders); 179 | } 180 | 181 | /** 182 | * @return {@code Sort} by matching client-side and server-side sorting properties 183 | */ 184 | private Sort mapSortFields() { 185 | if(pageRequestDTO.getSortFields() == null){ 186 | return Sort.unsorted(); 187 | } 188 | var orders = Arrays.stream(pageRequestDTO.getSortFields()) 189 | .map(sortDTO2OrderMapper()) 190 | .collect(toList()); 191 | return Sort.by(orders); 192 | } 193 | 194 | private Function sortDTO2OrderMapper() { 195 | return sortDTO -> { 196 | Objects.requireNonNull(dtoEntityMapping.get(sortDTO.getProperty()), 197 | () -> String.format("%s property must be bound via bindSort method!", sortDTO.getProperty())); 198 | return new Sort.Order(sortDTO.direction.toSortDirection(), 199 | dtoEntityMapping.get(sortDTO.getProperty()).getName()); 200 | }; 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/PageResultDTO.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto; 34 | 35 | import lombok.Data; 36 | import lombok.NoArgsConstructor; 37 | import org.springframework.data.domain.Page; 38 | 39 | import java.util.List; 40 | import java.util.function.Function; 41 | 42 | import static java.util.stream.Collectors.toList; 43 | 44 | /** 45 | * This DTO type is used to return query result pages from server to client. 46 | * By using {@code from} method, client and server can use different property names for dto and entities, e.g. 47 | *
48 |  *   PageResultDTO pageResultDTO = PageResultDTO.from(page, EmployeeMapper.INSTANCE::toDTO);
49 |  *  
50 | * or 51 | *
52 |  *   PageResultDTO pageResultDTO = PageResultDTO.from(page, e -> {
53 |  *             EmployeeResponseDTO dto = new EmployeeResponseDTO();
54 |  *             dto.setName(e.getName());
55 |  *             dto.setSurname(e.getSurname());
56 |  *             dto.setEmail(e.getEmail());
57 |  *             return dto;
58 |  *         });
59 |  * 
60 | */ 61 | @Data 62 | @NoArgsConstructor 63 | public class PageResultDTO { 64 | 65 | private List content; 66 | private long totalElements; 67 | private int currentPage; 68 | private int totalPages; 69 | private int size; 70 | 71 | /** 72 | * Creates {@code PageResultDTO} by converting Entity objects to DTO objects 73 | * @param page query result containing entity type 74 | * @param mapperFunction maps Entity type to DTO type 75 | * @param represents Entity type 76 | * @param represents DTO type 77 | * @return {@code PageResultDTO} 78 | */ 79 | public static PageResultDTO from(Page page, Function mapperFunction){ 80 | List resultDTOList = page.getContent().stream() 81 | .map(mapperFunction) 82 | .collect(toList()); 83 | PageResultDTO pageResultDTO = new PageResultDTO(); 84 | pageResultDTO.setContent(resultDTOList); 85 | pageResultDTO.setCurrentPage(page.getNumber()); 86 | pageResultDTO.setSize(page.getSize()); 87 | pageResultDTO.setTotalElements(page.getTotalElements()); 88 | pageResultDTO.setTotalPages(page.getTotalPages()); 89 | return pageResultDTO; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/RangeDTO.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto; 34 | 35 | import lombok.AllArgsConstructor; 36 | import lombok.Data; 37 | import lombok.NoArgsConstructor; 38 | 39 | /** 40 | * This DTO holds range values with properties named {@code low} and {@code high} 41 | */ 42 | @NoArgsConstructor 43 | @AllArgsConstructor 44 | @Data 45 | public class RangeDTO { 46 | private Object low; 47 | private Object high; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/operation/AbstractOperation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto.operation; 34 | import com.fasterxml.jackson.annotation.JsonSubTypes; 35 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 36 | import com.github.ozayduman.specificationbuilder.dto.Operator; 37 | import lombok.*; 38 | 39 | import java.util.EnumSet; 40 | import java.util.Objects; 41 | 42 | /** 43 | * It's the base class holding common properties and methods for all operation types. 44 | * All operations have {@code #property} and {@code #operator} but some operations have value, multi-value, range-value or no value as an operand depending on the operator. 45 | * 46 | */ 47 | @Getter 48 | @Setter 49 | @NoArgsConstructor 50 | @AllArgsConstructor 51 | @ToString 52 | @JsonTypeInfo( 53 | use = JsonTypeInfo.Id.NAME, 54 | include = JsonTypeInfo.As.EXISTING_PROPERTY, 55 | property = "operator", 56 | visible = true) 57 | @JsonSubTypes({ 58 | @JsonSubTypes.Type(value = SingleValueOperation.class, name = "EQ"), 59 | @JsonSubTypes.Type(value = SingleValueOperation.class, name = "NOT_EQ"), 60 | @JsonSubTypes.Type(value = SingleValueOperation.class, name = "GT"), 61 | @JsonSubTypes.Type(value = SingleValueOperation.class, name = "GE"), 62 | @JsonSubTypes.Type(value = SingleValueOperation.class, name = "LT"), 63 | @JsonSubTypes.Type(value = SingleValueOperation.class, name = "LE"), 64 | @JsonSubTypes.Type(value = RangeValueOperation.class, name = "BT"), 65 | @JsonSubTypes.Type(value = MultiValueOperation.class, name = "IN"), 66 | @JsonSubTypes.Type(value = MultiValueOperation.class, name = "NOT_IN"), 67 | @JsonSubTypes.Type(value = NoValueOperation.class, name = "NULL"), 68 | @JsonSubTypes.Type(value = NoValueOperation.class, name = "NOT_NULL"), 69 | @JsonSubTypes.Type(value = NoValueOperation.class, name = "TRUE"), 70 | @JsonSubTypes.Type(value = NoValueOperation.class, name = "FALSE"), 71 | }) 72 | 73 | public abstract class AbstractOperation { 74 | private String property; 75 | private Operator operator; 76 | 77 | /** 78 | * validates {@code property}, {@code operator}, and {@code #allowedOperators} 79 | */ 80 | public void validate(){ 81 | Objects.requireNonNull(property, () -> "Operation property can not be null"); 82 | Objects.requireNonNull(operator, () -> "operator can not be null"); 83 | validateOperator(allowedOperators()); 84 | } 85 | 86 | /** 87 | * @param allowedOperators checks validity of the operation by using {@code AbstractOperation#allowedOperators()} 88 | */ 89 | private void validateOperator(EnumSet allowedOperators) { 90 | if (!allowedOperators.contains(getOperator())) { 91 | throw new IllegalArgumentException(String.format("illegal operator %s", getOperator())); 92 | } 93 | } 94 | 95 | /** 96 | * @return allowed operations for the operator 97 | */ 98 | protected abstract EnumSet allowedOperators(); 99 | 100 | 101 | /** 102 | * @return value as {@code #Comparable[]} 103 | */ 104 | public abstract Comparable[] getOperands(); 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/operation/MultiValueOperation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto.operation; 34 | 35 | import com.github.ozayduman.specificationbuilder.dto.Operator; 36 | import lombok.Getter; 37 | import lombok.NoArgsConstructor; 38 | import lombok.Setter; 39 | import lombok.ToString; 40 | 41 | import java.util.EnumSet; 42 | import java.util.Objects; 43 | 44 | import static com.github.ozayduman.specificationbuilder.dto.Operator.IN; 45 | import static com.github.ozayduman.specificationbuilder.dto.Operator.NOT_IN; 46 | 47 | /** 48 | * This operation type used to hold multiple values that the used operator needs. 49 | */ 50 | @NoArgsConstructor 51 | @Getter 52 | @Setter 53 | @ToString 54 | public class MultiValueOperation extends AbstractOperation { 55 | private Object[] value; 56 | 57 | /** 58 | * @param property name of the Operation property 59 | * @param operator {@code Operator} 60 | * @param value operands that operator works on 61 | */ 62 | public MultiValueOperation(String property, Operator operator, Object[] value) { 63 | super(property, operator); 64 | this.value = value; 65 | } 66 | 67 | @Override 68 | public void validate() { 69 | super.validate(); 70 | Objects.requireNonNull(value, () -> "value can not be null!"); 71 | } 72 | 73 | @Override 74 | protected EnumSet allowedOperators() { 75 | return EnumSet.of(IN, NOT_IN); 76 | } 77 | 78 | @Override 79 | public Comparable[] getOperands() { 80 | return (Comparable[]) value; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/operation/NoValueOperation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto.operation; 34 | 35 | import com.github.ozayduman.specificationbuilder.dto.Operator; 36 | import lombok.NoArgsConstructor; 37 | import lombok.ToString; 38 | 39 | import java.util.EnumSet; 40 | 41 | import static com.github.ozayduman.specificationbuilder.dto.Operator.*; 42 | 43 | /** 44 | * This type represents operation that does not need a value such as null, true, or false. 45 | */ 46 | @NoArgsConstructor 47 | @ToString 48 | public class NoValueOperation extends AbstractOperation { 49 | 50 | /** 51 | * @param property name of the Operation property 52 | * @param operator {@code Operator} 53 | */ 54 | public NoValueOperation(String property, Operator operator) { 55 | super(property, operator); 56 | } 57 | 58 | @Override 59 | protected EnumSet allowedOperators() { 60 | return EnumSet.of(NULL, NOT_NULL, TRUE, FALSE); 61 | } 62 | 63 | @Override 64 | public Comparable[] getOperands() { 65 | return new Comparable[0]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/operation/RangeValueOperation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto.operation; 34 | 35 | import com.github.ozayduman.specificationbuilder.dto.Operator; 36 | import com.github.ozayduman.specificationbuilder.dto.RangeDTO; 37 | import lombok.Getter; 38 | import lombok.NoArgsConstructor; 39 | import lombok.Setter; 40 | import lombok.ToString; 41 | 42 | import java.util.EnumSet; 43 | import java.util.Objects; 44 | 45 | import static com.github.ozayduman.specificationbuilder.dto.Operator.BT; 46 | 47 | /** 48 | * This type represents operations that need a range of values (e.g. between). 49 | */ 50 | @NoArgsConstructor 51 | @Getter 52 | @Setter 53 | @ToString 54 | public class RangeValueOperation extends AbstractOperation { 55 | private RangeDTO value; 56 | /** 57 | * @param property name of the Operation property 58 | * @param operator {@code Operator} 59 | * @param value {@code RangeDTO} operand that operator works on 60 | */ 61 | public RangeValueOperation(String property, Operator operator, RangeDTO value) { 62 | super(property, operator); 63 | this.value = value; 64 | } 65 | 66 | @Override 67 | public void validate() { 68 | super.validate(); 69 | Objects.requireNonNull(value, () -> "value can not be null!"); 70 | Objects.requireNonNull(value.getLow(), () -> "low value can not be null!"); 71 | Objects.requireNonNull(value.getHigh(), () -> "high value can not be null!"); 72 | } 73 | 74 | @Override 75 | protected EnumSet allowedOperators() { 76 | return EnumSet.of(BT); 77 | } 78 | 79 | @Override 80 | public Comparable[] getOperands() { 81 | return new Comparable[]{(Comparable) value.getLow(), (Comparable) value.getHigh()}; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/operation/SingleValueOperation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.dto.operation; 34 | 35 | import com.github.ozayduman.specificationbuilder.dto.Operator; 36 | import lombok.*; 37 | import org.springframework.util.Assert; 38 | 39 | import java.util.EnumSet; 40 | import java.util.Objects; 41 | 42 | import static com.github.ozayduman.specificationbuilder.dto.Operator.*; 43 | 44 | /** 45 | * This type represents operations that works on a single value (e.g. equal, greater etc.). 46 | */ 47 | @NoArgsConstructor 48 | @Getter 49 | @Setter 50 | @ToString 51 | public class SingleValueOperation extends AbstractOperation { 52 | private Object value; 53 | 54 | /** 55 | * @param property name of the Operation property 56 | * @param operator {@code Operator} 57 | * @param value operands that operator works on 58 | */ 59 | public SingleValueOperation(String property, Operator operator, Object value) { 60 | super(property, operator); 61 | this.value = value; 62 | } 63 | 64 | @Override 65 | public void validate() { 66 | super.validate(); 67 | Objects.requireNonNull(value, () -> "value can not be null!"); 68 | validateIfOperatorIsLike(); 69 | } 70 | 71 | @Override 72 | protected EnumSet allowedOperators() { 73 | return EnumSet.of(EQ,NOT_EQ,GT,GE,LT,LE,LIKE,NOT_LIKE); 74 | } 75 | 76 | @Override 77 | public Comparable[] getOperands() { 78 | return new Comparable[]{(Comparable) value}; 79 | } 80 | 81 | /** 82 | * When given operator is like or not like then operand's type of value must be 'String' 83 | * otherwise throws {@link IllegalArgumentException} 84 | */ 85 | private void validateIfOperatorIsLike() { 86 | if(EnumSet.of(LIKE, NOT_LIKE).contains(getOperator())){ 87 | Assert.isInstanceOf(String.class, value, 88 | () -> String.format("type of the given value must be 'String'! instead of %s", value.getClass())); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/operation/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | /** 34 | * This package contains operation dto (Data Transfer Object) types transferred from client to server. 35 | */ 36 | 37 | package com.github.ozayduman.specificationbuilder.dto.operation; 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/dto/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | /** 34 | * This package contains core dto (data transfer object) types like {@link com.github.ozayduman.specificationbuilder.dto.PageRequestDTO}, {@link com.github.ozayduman.specificationbuilder.dto.PageResultDTO} and also {@link com.github.ozayduman.specificationbuilder.dto.operation} sub package. 35 | */ 36 | package com.github.ozayduman.specificationbuilder.dto; 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/ozayduman/specificationbuilder/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | /** 34 | * This package contains {@link com.github.ozayduman.specificationbuilder.SpecificationMappings.SpecificationBuilder} 35 | * and related types to bind dto and entity properties to be used in dynamic query generation. 36 | */ 37 | package com.github.ozayduman.specificationbuilder; 38 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/SpecificationBuilderIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder; 2 | 3 | import com.github.ozayduman.specificationbuilder.dto.*; 4 | import com.github.ozayduman.specificationbuilder.dto.PageRequestDTO.PageRequestBuilder; 5 | import com.github.ozayduman.specificationbuilder.dto.PageRequestDTO.SortDTO; 6 | import com.github.ozayduman.specificationbuilder.dto.operation.NoValueOperation; 7 | import com.github.ozayduman.specificationbuilder.dto.operation.SingleValueOperation; 8 | import com.github.ozayduman.specificationbuilder.entity.*; 9 | import com.github.ozayduman.specificationbuilder.repository.EmployeeRepository; 10 | import lombok.val; 11 | import org.junit.jupiter.api.Disabled; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.Mockito; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 17 | import org.springframework.data.domain.Page; 18 | import org.springframework.data.jpa.domain.Specification; 19 | import org.springframework.test.context.ContextConfiguration; 20 | import org.springframework.test.context.junit.jupiter.SpringExtension; 21 | 22 | import javax.persistence.criteria.*; 23 | import java.time.LocalDate; 24 | import java.time.Month; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | import java.util.NoSuchElementException; 28 | import java.util.stream.Collectors; 29 | 30 | import static com.github.ozayduman.specificationbuilder.SpecificationMappings.SpecificationBuilder; 31 | import static com.github.ozayduman.specificationbuilder.entity.SocialSecurityType.A; 32 | import static java.util.Comparator.comparing; 33 | import static org.junit.jupiter.api.Assertions.*; 34 | import static org.mockito.Mockito.*; 35 | 36 | @DataJpaTest 37 | @ExtendWith(SpringExtension.class) 38 | @ContextConfiguration(classes = {TestConfiguration.class}) 39 | class SpecificationBuilderIntegrationTest { 40 | 41 | @Autowired 42 | EmployeeRepository employeeRepository; 43 | 44 | @Test 45 | void shouldBuildASpecificationWithDTO() { 46 | assertAll("build", 47 | () -> assertThrows(RuntimeException.class, () -> SpecificationBuilder.of(null).build()), 48 | () -> assertNotNull(SpecificationBuilder.of(new CriteriaDTO()).build()) 49 | ); 50 | } 51 | 52 | @Test 53 | void whenOperationInvalidThenAnIllegalStateExceptionThrown() { 54 | final var criteriaDTO = new CriteriaDTO(); 55 | criteriaDTO.setOperations(List.of(new NoValueOperation("name", Operator.EQ))); 56 | assertThrows(IllegalArgumentException.class, () -> SpecificationBuilder.of(criteriaDTO).build()); 57 | } 58 | 59 | @Test 60 | @Disabled 61 | void whenEQPramsProvidedThenEQSpecCreated() { 62 | 63 | final CriteriaDTO criteriaDTO = new CriteriaDTO(); 64 | final String valueOfName = "Ozay"; 65 | var operation = new SingleValueOperation("name", Operator.EQ, valueOfName); 66 | criteriaDTO.setOperations(List.of(operation)); 67 | 68 | 69 | final Specification specification = SpecificationBuilder.of(criteriaDTO) 70 | .bind("name", Employee_.name) 71 | .build(); 72 | Root rootMock = mock(Root.class); 73 | Path namePathMock = mock(Path.class); 74 | when(rootMock.get(Employee_.name)).thenReturn(namePathMock); 75 | CriteriaQuery criteriaQueryMock = mock(CriteriaQuery.class); 76 | CriteriaBuilder criteriaBuilderMock = mock(CriteriaBuilder.class); 77 | Predicate namePredicateMock = mock(Predicate.class); 78 | when(criteriaBuilderMock.equal(Mockito.any(), Mockito.any())).thenReturn(namePredicateMock); 79 | 80 | final Predicate predicate = specification.toPredicate(rootMock, criteriaQueryMock, criteriaBuilderMock); 81 | 82 | verify(rootMock, times(1)).get(Employee_.name); 83 | verifyNoMoreInteractions(rootMock); 84 | verify(criteriaBuilderMock, times(1)).equal(namePathMock, valueOfName); 85 | verify(criteriaBuilderMock, never()).conjunction(); 86 | verifyNoInteractions(criteriaQueryMock); 87 | } 88 | 89 | @Test 90 | void whenDTOPropertyNotBoundThenExceptionThrown() { 91 | final CriteriaDTO criteriaDTO = new CriteriaDTO(); 92 | final String valueOfLastName = "Duman"; 93 | var operation = new SingleValueOperation("surname", Operator.EQ, valueOfLastName); 94 | criteriaDTO.setOperations(List.of(operation)); 95 | 96 | 97 | final Specification specification = SpecificationBuilder.of(criteriaDTO) 98 | .bind("name", Employee_.name) 99 | .build(); 100 | 101 | Root rootMock = mock(Root.class); 102 | CriteriaQuery criteriaQueryMock = mock(CriteriaQuery.class); 103 | CriteriaBuilder criteriaBuilderMock = mock(CriteriaBuilder.class); 104 | 105 | assertThrows(RuntimeException.class, 106 | () -> specification.toPredicate(rootMock, criteriaQueryMock, criteriaBuilderMock)); 107 | } 108 | 109 | @Test 110 | void whenMultiEQPramsProvidedThenEQSpecCreated() { 111 | final CriteriaDTO criteriaDTO = new CriteriaDTO(); 112 | final String valueOfName = "Ozay"; 113 | final String valueOfLastName = "Duman"; 114 | var operationForName = new SingleValueOperation("name", Operator.EQ, valueOfName); 115 | var operationForSurname = new SingleValueOperation("surname", Operator.EQ, valueOfLastName); 116 | criteriaDTO.setOperations(List.of(operationForName, operationForSurname)); 117 | 118 | final Specification specification = SpecificationBuilder.of(criteriaDTO) 119 | .bind("name", Employee_.name) 120 | .bind("surname", Employee_.surname) 121 | .build(); 122 | Root rootMock = mock(Root.class); 123 | Path namePathMock = mock(Path.class); 124 | Path surnamePathMock = mock(Path.class); 125 | when(rootMock.get(Employee_.name.getName())).thenReturn(namePathMock); 126 | when(rootMock.get(Employee_.surname.getName())).thenReturn(surnamePathMock); 127 | CriteriaQuery criteriaQueryMock = mock(CriteriaQuery.class); 128 | CriteriaBuilder criteriaBuilderMock = mock(CriteriaBuilder.class); 129 | Predicate namePredicateMock = mock(Predicate.class); 130 | Predicate surnamePredicateMock = mock(Predicate.class); 131 | when(criteriaBuilderMock.equal(Mockito.any(), eq(valueOfName))).thenReturn(namePredicateMock); 132 | when(criteriaBuilderMock.equal(Mockito.any(), eq(valueOfLastName))).thenReturn(surnamePredicateMock); 133 | 134 | final Predicate predicate = specification.toPredicate(rootMock, criteriaQueryMock, criteriaBuilderMock); 135 | 136 | verify(rootMock, times(1)).get(Employee_.name.getName()); 137 | verify(rootMock, times(1)).get(Employee_.surname.getName()); 138 | verifyNoMoreInteractions(rootMock); 139 | verify(criteriaBuilderMock, times(1)).equal(namePathMock, valueOfName); 140 | verify(criteriaBuilderMock, times(1)).equal(surnamePathMock, valueOfLastName); 141 | verify(criteriaBuilderMock, never()).conjunction(); 142 | } 143 | 144 | /* @Test 145 | void whenRangePramsProvidedThenBetweenSpecCreated() { 146 | final CriteriaDTO criteriaDTO = new CriteriaDTO(); 147 | RangeDTO valueOfBirdDate = new RangeDTO(LocalDate.now().minusYears(15L), LocalDate.now()); 148 | criteriaDTO.setBetween(Map.of("birthDate", valueOfBirdDate)); 149 | 150 | final Specification specification = SpecificationBuilder.of(criteriaDTO) 151 | .bind("birthDate", Employee_.birthDate) 152 | .build(); 153 | Root rootMock = mock(Root.class); 154 | Path birthPathMock = mock(Path.class); 155 | when(rootMock.get(Employee_.birthDate.getName())).thenReturn(birthPathMock); 156 | CriteriaQuery criteriaQueryMock = mock(CriteriaQuery.class); 157 | CriteriaBuilder criteriaBuilderMock = mock(CriteriaBuilder.class); 158 | Predicate betweenPredicateMock = mock(Predicate.class); 159 | when(criteriaBuilderMock.between(Mockito.any(Expression.class),eq((Comparable)valueOfBirdDate.getFirst()),eq((Comparable)valueOfBirdDate.getSecond()))).thenReturn(betweenPredicateMock); 160 | 161 | final Predicate predicate = specification.toPredicate(rootMock, criteriaQueryMock, criteriaBuilderMock); 162 | 163 | verify(rootMock, times(1)).get(Employee_.birthDate.getName()); 164 | verifyNoMoreInteractions(rootMock); 165 | verify(criteriaBuilderMock, times(1)).between(birthPathMock, valueOfBirdDate.getFirst(), valueOfBirdDate.getSecond()); 166 | verify(criteriaBuilderMock,never()).conjunction(); 167 | verifyNoInteractions(criteriaQueryMock); 168 | }*/ 169 | 170 | @Test 171 | void whenDTOPropertyBoundViaJoinThenJoinSpecExecuted() { 172 | 173 | var employee = new Employee("özay", "duman", "ozay.duman@gmail.com", LocalDate.now().minusYears(20L)); 174 | final var phoneBusiness = Phone.builder() 175 | .number("5555") 176 | .phoneType(PhoneType.BUSSINES) 177 | .build(); 178 | final var phoneHome = Phone.builder() 179 | .number("55555") 180 | .phoneType(PhoneType.HOME) 181 | .build(); 182 | employee.addPhone(phoneBusiness); 183 | employee.addPhone(phoneHome); 184 | employeeRepository.save(employee); 185 | 186 | final CriteriaDTO criteriaDTO = new CriteriaDTO(); 187 | var operation = new SingleValueOperation("phoneNumber", Operator.EQ, "5555"); 188 | criteriaDTO.setOperations(List.of(operation)); 189 | 190 | final Specification specification = SpecificationBuilder.of(criteriaDTO) 191 | .bindJoin("phoneNumber", Employee_.phones, Phone_.number) 192 | .build(); 193 | 194 | var customerFromDB = employeeRepository.findOne(specification) 195 | .orElseThrow(() -> new NoSuchElementException()); 196 | 197 | assertNotNull(customerFromDB); 198 | assertEquals("özay", customerFromDB.getName()); 199 | assertEquals("duman", customerFromDB.getSurname()); 200 | assertEquals("ozay.duman@gmail.com", customerFromDB.getEmail()); 201 | } 202 | 203 | @Test 204 | void whenSortDTOSuppliedThenResultSortedByGivenSortInfo() { 205 | final var employees = TestDataGenerator.createEmployees(); 206 | employeeRepository.saveAll(employees); 207 | final PageRequestDTO pageRequestDTO = new PageRequestDTO(); 208 | pageRequestDTO.setOperations(List.of( 209 | new SingleValueOperation("employeeBirthDate", Operator.GT, LocalDate.of(2000, Month.JANUARY, 1)))); 210 | pageRequestDTO.setPage(0); 211 | pageRequestDTO.setSize(10); 212 | pageRequestDTO.setSortFields(new SortDTO[]{ 213 | new SortDTO("employeeBirthDate", SortDTO.Direction.ASC), 214 | new SortDTO("employeeName", SortDTO.Direction.ASC) 215 | }); 216 | 217 | final Specification specification = SpecificationBuilder.of(pageRequestDTO) 218 | .bind("employeeName", Employee_.name) 219 | .bind("employeeSurname", Employee_.surname) 220 | .bind("employeeEmail", Employee_.email) 221 | .bind("employeeBirthDate", Employee_.birthDate) 222 | .bindJoin("phoneNumber", Employee_.phones, Phone_.number) 223 | .build(); 224 | 225 | var pageRequest = PageRequestBuilder.of(pageRequestDTO) 226 | .bindSort("employeeBirthDate", Employee_.birthDate) 227 | .bindSort("employeeName", Employee_.name) 228 | .build(); 229 | 230 | Page page = employeeRepository.findAll(specification, pageRequest); 231 | 232 | PageResultDTO pageResultDTO = PageResultDTO.from(page, e -> { 233 | EmployeeResponseDTO dto = new EmployeeResponseDTO(); 234 | dto.setName(e.getName()); 235 | dto.setLastName(e.getSurname()); 236 | dto.setEmail(e.getEmail()); 237 | dto.setBirthDate(e.getBirthDate()); 238 | return dto; 239 | }); 240 | 241 | //PageResultDTO pageResultDTO = PageResultDTO.from(page, EmployeeMapper.INSTANCE::toDTO); 242 | 243 | assertNotNull(pageResultDTO); 244 | assertEquals(0, pageRequestDTO.getPage()); 245 | assertEquals(10, pageRequestDTO.getSize()); 246 | 247 | final var originalContent = (List) pageResultDTO.getContent(); 248 | final var employeeResponseDTOS = new ArrayList<>(originalContent); 249 | final var sortedEmployeeDTOs = employeeResponseDTOS.stream() 250 | .sorted( 251 | comparing(EmployeeResponseDTO::getBirthDate) 252 | .thenComparing(EmployeeResponseDTO::getName)) 253 | .collect(Collectors.toList()); 254 | for (int i = 0; i < originalContent.size(); i++) { 255 | assertEquals(originalContent.get(i), sortedEmployeeDTOs.get(i)); 256 | } 257 | } 258 | 259 | @Test 260 | void whenLikeOperatorGivenThenResultRestrictedByLikeProperty() { 261 | final var employees = TestDataGenerator.createEmployees(); 262 | employeeRepository.saveAll(employees); 263 | final CriteriaDTO criteriaDTO = new CriteriaDTO(); 264 | criteriaDTO.setOperations(List.of( 265 | new SingleValueOperation("name", Operator.LIKE, "s"))); 266 | val specification = SpecificationBuilder.of(criteriaDTO) 267 | .bind(Employee_.name) 268 | .bind(Employee_.surname) 269 | .bind(Employee_.email) 270 | .bind(Employee_.birthDate) 271 | .build(); 272 | val allEmployees = employeeRepository.findAll(specification); 273 | assertTrue(allEmployees.size() > 0); 274 | val hasNamesNotContainingValue = allEmployees.stream() 275 | .anyMatch(employee -> !employee.getName().contains("s")); 276 | assertFalse(hasNamesNotContainingValue); 277 | } 278 | 279 | @Test 280 | void whenNotLikeOperatorGivenThenResultRestrictedByNotLikeProperty() { 281 | final var employees = TestDataGenerator.createEmployees(); 282 | employeeRepository.saveAll(employees); 283 | final CriteriaDTO criteriaDTO = new CriteriaDTO(); 284 | criteriaDTO.setOperations(List.of( 285 | new SingleValueOperation("name", Operator.NOT_LIKE, "s"))); 286 | val specification = SpecificationBuilder.of(criteriaDTO) 287 | .bind(Employee_.name) 288 | .bind(Employee_.surname) 289 | .bind(Employee_.email) 290 | .bind(Employee_.birthDate) 291 | .build(); 292 | val allEmployees = employeeRepository.findAll(specification); 293 | assertTrue(allEmployees.size() > 0); 294 | val hasNamesContainingValue = allEmployees.stream() 295 | .anyMatch(employee -> employee.getName().contains("s")); 296 | assertFalse(hasNamesContainingValue); 297 | } 298 | 299 | @Test 300 | void whenOperatorGivenThenResultRestrictedByNotLikeProperty() { 301 | final var socialSecurity = SocialSecurity.of(A, "AAA"); 302 | final var employee = Employee.of("özay", "duman", "o.d@test.com", 303 | LocalDate.of(1900, Month.JANUARY, 1), socialSecurity); 304 | employeeRepository.save(employee); 305 | final CriteriaDTO criteriaDTO = new CriteriaDTO(); 306 | criteriaDTO.setOperations(List.of( 307 | new SingleValueOperation("name", Operator.NOT_LIKE, "s"))); 308 | val specification = SpecificationBuilder.of(criteriaDTO) 309 | .bind(Employee_.name) 310 | .bind(Employee_.surname) 311 | .bind(Employee_.email) 312 | .bind(Employee_.birthDate) 313 | .bindJoin(Employee_.socialSecurity, SocialSecurity_.socialSecurityType) 314 | .build(); 315 | val allEmployees = employeeRepository.findAll(specification); 316 | assertTrue(allEmployees.size() > 0); 317 | val hasNamesContainingValue = allEmployees.stream() 318 | .anyMatch(e -> e.getName().contains("s")); 319 | assertFalse(hasNamesContainingValue); 320 | } 321 | 322 | } 323 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/TestConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder; 2 | 3 | import org.springframework.boot.autoconfigure.domain.EntityScan; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 7 | import org.springframework.test.context.TestPropertySource; 8 | 9 | @Configuration 10 | @EntityScan(basePackages = {"com.github.ozayduman.specificationbuilder.entity"}) 11 | @TestPropertySource("classpath:application.properties") 12 | @EnableJpaRepositories(basePackages = {"com.github.ozayduman.specificationbuilder.repository"}) 13 | public class TestConfiguration {} 14 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/TestDataGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder; 2 | 3 | import com.github.ozayduman.specificationbuilder.entity.Employee; 4 | import lombok.experimental.UtilityClass; 5 | 6 | import java.time.LocalDate; 7 | import java.time.format.DateTimeFormatter; 8 | import java.util.List; 9 | 10 | @UtilityClass 11 | public class TestDataGenerator { 12 | 13 | public Employee create(String name, String surname, String email, String birdDate){ 14 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/d"); 15 | return new Employee(name,surname,email, LocalDate.parse(birdDate,formatter)); 16 | } 17 | 18 | public List createEmployees(){ 19 | return List.of( 20 | create("Doloritas", "Yewdell", "dyewdell3@earthlink.net", "1991/06/11"), 21 | create("April", "Cargill", "acargill4@i2i.jp", "2020/08/10"), 22 | create("Ermina", "Chisnell", "echisnell5@ycombinator.com", "2007/08/13"), 23 | create("Dominik", "Gyngyll", "dgyngyll6@wix.com", "1991/08/23"), 24 | create("Marcy", "Schwaiger", "mschwaiger7@diigo.com", "1994/04/14"), 25 | create("Abbey", "Muddicliffe", "amuddicliffe8@wufoo.com", "2005/06/19"), 26 | create("Ebony", "Richardeau", "erichardeau9@bluehost.com", "2004/11/15"), 27 | create("Falkner", "McBay", "fmcbaya@mozilla.org", "1986/03/30"), 28 | create("Farra", "Tatnell", "ftatnellb@symantec.com", "2001/02/24"), 29 | create("Filippa", "Willows", "fwillowsc@sbwire.com", "1988/09/20"), 30 | create("Marietta", "Trowsdale", "mtrowsdaled@mayoclinic.com", "2004/05/04"), 31 | create("Iggy", "Yanson", "iyansone@forbes.com", "1987/03/23"), 32 | create("Margy", "Bechley", "mbechleyf@nifty.com", "1986/05/26"), 33 | create("Norman", "Gresch", "ngreschg@parallels.com", "2008/10/19"), 34 | create("Tamiko", "MacManus", "tmacmanush@ask.com", "1993/09/15"), 35 | create("Hillie", "Conklin", "hconklini@google.com.br", "2013/02/18"), 36 | create("Loren", "Neaverson", "lneaversonj@woothemes.com", "1994/05/05"), 37 | create("Cammie", "Perago", "cperagok@gravatar.com", "2012/04/07"), 38 | create("Salim", "Ganing", "sganingl@infoseek.co.jp", "1998/03/17"), 39 | create("Kass", "Coltherd", "kcoltherdm@techcrunch.com", "1996/11/26"), 40 | create("Carla", "Lutas", "clutasn@usnews.com", "2001/10/03"), 41 | create("Standford", "Badwick", "sbadwicko@rediff.com", "2011/01/07"), 42 | create("Lorne", "Mewis", "lmewisp@alexa.com", "1993/10/15"), 43 | create("Ric", "Quaife", "rquaifeq@dot.gov", "2019/05/15"), 44 | create("Paulina", "Benjafield", "pbenjafieldr@wiley.com", "2006/12/04"), 45 | create("Grant", "Bahl", "gbahls@hatena.ne.jp", "2001/12/09"), 46 | create("Julieta", "Greenroyd", "jgreenroydt@deviantart.com", "1997/03/11"), 47 | create("Freemon", "Roth", "frothu@mayoclinic.com", "2013/06/10"), 48 | create("Alice", "Bentz", "abentzv@icq.com", "2008/08/05"), 49 | create("Ruthanne", "Haking", "rhakingw@telegraph.co.uk", "2011/03/22"), 50 | create("Cedric", "Antoniutti", "cantoniuttix@nasa.gov", "2002/01/26"), 51 | create("Sydney", "Maddison", "smaddisony@bloomberg.com", "1999/10/10"), 52 | create("Elsy", "McClymont", "emcclymontz@google.it", "2005/08/30"), 53 | create("Marshal", "Ripping", "mripping10@wsj.com", "1992/07/25"), 54 | create("Luce", "Sparrow", "lsparrow11@usa.gov", "1990/07/25"), 55 | create("Blinny", "Lusk", "blusk12@squidoo.com", "1994/10/07") 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/TestUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.json.JsonMapper; 5 | import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; 6 | import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; 7 | import lombok.experimental.UtilityClass; 8 | 9 | @UtilityClass 10 | public class TestUtil { 11 | 12 | public ObjectMapper createObjectMapper() { 13 | PolymorphicTypeValidator ptv = 14 | BasicPolymorphicTypeValidator.builder() 15 | .allowIfSubType("com.github.ozayduman.specificationbuilder.dto.operation") 16 | .build(); 17 | ObjectMapper objectMapper = JsonMapper.builder() 18 | .activateDefaultTyping(ptv) 19 | .build(); 20 | return objectMapper; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/dto/EmployeeResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.dto; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.time.LocalDate; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | public class EmployeeResponseDTO { 11 | private String name, lastName, email; 12 | private LocalDate birthDate; 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/dto/PageRequestDTOTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.dto; 2 | 3 | import com.github.ozayduman.specificationbuilder.dto.PageRequestDTO.PageRequestBuilder; 4 | import com.github.ozayduman.specificationbuilder.dto.PageRequestDTO.SortDTO; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.data.domain.Sort; 7 | 8 | import javax.persistence.metamodel.SingularAttribute; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | import static org.mockito.Mockito.mock; 12 | import static org.mockito.Mockito.when; 13 | 14 | class PageRequestDTOTest { 15 | 16 | @Test 17 | void whenPageSizeNotSuppliedThen20AsDefaultWillBeUsed() { 18 | final var pageRequestDTO = new PageRequestDTO(); 19 | final var pageRequest = PageRequestBuilder.of(pageRequestDTO) 20 | .build(); 21 | assertEquals(20, pageRequest.getPageSize()); 22 | } 23 | 24 | @Test 25 | void whenSortOrderNotSpecifiedThenUnOrderedSortingWillBeUsed() { 26 | final var pageRequestDTO = new PageRequestDTO(); 27 | final var pageRequest = PageRequestBuilder.of(pageRequestDTO) 28 | .build(); 29 | assertEquals(Sort.unsorted(), pageRequest.getSort()); 30 | } 31 | 32 | @Test 33 | void whenPageSizeSuppliedThenItWillBeUsed() { 34 | final var pageRequestDTO = new PageRequestDTO(); 35 | pageRequestDTO.setPage(3); 36 | pageRequestDTO.setSize(10); 37 | final var pageRequest = PageRequestBuilder.of(pageRequestDTO) 38 | .build(); 39 | assertAll( 40 | () -> assertEquals(3, pageRequest.getPageNumber()), 41 | () -> assertEquals(10, pageRequest.getPageSize()) 42 | ); 43 | } 44 | 45 | @Test 46 | void whenSortDTONotBoundThenExceptionThrown() { 47 | final var pageRequestDTO = new PageRequestDTO(); 48 | pageRequestDTO.setSortFields(new SortDTO[]{new SortDTO("name", SortDTO.Direction.DESC)}); 49 | assertThrows(NullPointerException.class,() -> PageRequestBuilder.of(pageRequestDTO) 50 | .build()); 51 | } 52 | 53 | @Test 54 | void shouldMapSortPropertyToEntityProperty() { 55 | final var pageRequestDTO = new PageRequestDTO(); 56 | pageRequestDTO.setSortFields(new SortDTO[]{new SortDTO("name", SortDTO.Direction.DESC)}); 57 | SingularAttribute> metaModelField = mock(SingularAttribute.class); 58 | when(metaModelField.getName()).thenReturn("name"); 59 | final var pageRequest = PageRequestBuilder.of(pageRequestDTO) 60 | .bindSort("name", metaModelField) 61 | .build(); 62 | assertTrue(pageRequest.getSort().isSorted()); 63 | final Sort.Order nameOrder = pageRequest.getSort().getOrderFor("name"); 64 | assertAll( 65 | () -> assertEquals("name", nameOrder.getProperty()), 66 | () -> assertEquals(Sort.Direction.DESC, nameOrder.getDirection()) 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/dto/PageResultDTOTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.EqualsAndHashCode; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mock; 7 | import org.mockito.Mockito; 8 | import org.springframework.data.domain.Page; 9 | 10 | import java.util.List; 11 | 12 | import static org.junit.jupiter.api.Assertions.*; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.when; 15 | 16 | class PageResultDTOTest { 17 | 18 | private static final int CURRENT_PAGE = 0; 19 | private static final int PAGE_SIZE = 3; 20 | private static final long TOTAL_ELEMENTS = 3L; 21 | private static final int TOTAL_PAGES = 100; 22 | 23 | @Test 24 | void shouldMapEntityToDTOfromPage() { 25 | Page page = mock(Page.class); 26 | List entityList = List.of(Entity.of("e1"), Entity.of("e2"), Entity.of("e3")); 27 | when(page.getContent()).thenReturn(entityList); 28 | when(page.getNumber()).thenReturn(CURRENT_PAGE); 29 | when(page.getSize()).thenReturn(PAGE_SIZE); 30 | when(page.getTotalElements()).thenReturn(TOTAL_ELEMENTS); 31 | when(page.getTotalPages()).thenReturn(TOTAL_PAGES); 32 | final PageResultDTO pageResultDTO = PageResultDTO.from(page, (Entity e) -> DTO.of(e.entityProperty)); 33 | 34 | assertAll( 35 | () -> assertEquals(CURRENT_PAGE, pageResultDTO.getCurrentPage()), 36 | () -> assertEquals(PAGE_SIZE, pageResultDTO.getSize()), 37 | () -> assertEquals(TOTAL_ELEMENTS, pageResultDTO.getTotalElements()), 38 | () -> assertEquals(TOTAL_PAGES, pageResultDTO.getTotalPages()), 39 | () -> assertTrue(pageResultDTO.getContent().containsAll(List.of(DTO.of("e1"), DTO.of("e2"), DTO.of("e3")))) 40 | ); 41 | 42 | } 43 | 44 | @AllArgsConstructor 45 | static class Entity { 46 | String entityProperty; 47 | static Entity of(String property){ 48 | return new Entity(property); 49 | } 50 | } 51 | 52 | @AllArgsConstructor 53 | @EqualsAndHashCode 54 | static class DTO{ 55 | String dtoProperty; 56 | static DTO of(String property){ 57 | return new DTO(property); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/dto/operation/AbstractOperationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.dto.operation; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.json.JsonMapper; 6 | import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; 7 | import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; 8 | import com.github.ozayduman.specificationbuilder.TestUtil; 9 | import com.github.ozayduman.specificationbuilder.dto.Operator; 10 | import lombok.AllArgsConstructor; 11 | import org.junit.jupiter.api.Disabled; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.util.EnumSet; 15 | 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | class AbstractOperationTest { 19 | 20 | @Test 21 | void whenPropertyAndOperatorNUllThenExceptionThrown() { 22 | assertThrows(NullPointerException.class, () -> new CustomOperation().validate()); 23 | } 24 | 25 | @Test 26 | void whenOperatorIsNotAllowedThenExceptionThrown() { 27 | final var customOperation = new CustomOperation(); 28 | customOperation.setOperator(Operator.EQ); 29 | customOperation.setProperty("property"); 30 | assertThrows(IllegalArgumentException.class, () -> customOperation.validate()); 31 | } 32 | 33 | @Test 34 | void whenOperatorHavingPropertyAndAllowedOperatorThenValidated() { 35 | final var customOperation = new CustomOperation("property", Operator.NULL); 36 | assertDoesNotThrow(() -> customOperation.validate()); 37 | } 38 | 39 | @Test 40 | void shouldDeserializeToCorrectOperationType() throws JsonProcessingException { 41 | ObjectMapper objectMapper = TestUtil.createObjectMapper(); 42 | var json = "{\"property\": \"phoneNumber\",\"operator\": \"NOT_NULL\"}"; 43 | final var operation = objectMapper.readValue(json, AbstractOperation.class); 44 | assertTrue(operation instanceof NoValueOperation); 45 | assertEquals("phoneNumber",operation.getProperty()); 46 | assertEquals(Operator.NOT_NULL, operation.getOperator()); 47 | } 48 | 49 | class CustomOperation extends AbstractOperation{ 50 | public CustomOperation() {} 51 | 52 | public CustomOperation(String property, Operator operator) { 53 | super(property, operator); 54 | } 55 | 56 | @Override 57 | protected EnumSet allowedOperators() { 58 | return EnumSet.of(Operator.NULL); 59 | } 60 | 61 | @Override 62 | public Comparable[] getOperands() { 63 | return new Comparable[0]; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/dto/operation/MultiValueOperationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.dto.operation; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.ozayduman.specificationbuilder.TestUtil; 6 | import com.github.ozayduman.specificationbuilder.dto.Operator; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.time.LocalDate; 10 | import java.util.Arrays; 11 | import java.util.EnumSet; 12 | import java.util.List; 13 | 14 | import static com.github.ozayduman.specificationbuilder.dto.Operator.*; 15 | import static org.junit.jupiter.api.Assertions.*; 16 | 17 | class MultiValueOperationTest { 18 | @Test 19 | void shouldAllowTheseOperators() { 20 | final var operation = new MultiValueOperation(); 21 | final var operators = operation.allowedOperators(); 22 | assertTrue(operators.containsAll(List.of(IN, NOT_IN))); 23 | } 24 | 25 | @Test 26 | void whenAllowedOperatorSuppliedThenOperationValidated() { 27 | final var operation = new MultiValueOperation("birthDate", IN, new LocalDate[]{LocalDate.now().minusYears(65), LocalDate.now().minusYears(18)}); 28 | assertDoesNotThrow(() -> operation.validate()); 29 | } 30 | 31 | @Test 32 | void whenNotAllowedOperatorSuppliedThenIllegalArgumentExcThrown() { 33 | final var operation = new MultiValueOperation("birthDate", EQ, new LocalDate[]{LocalDate.now().minusYears(65), LocalDate.now().minusYears(18)}); 34 | assertThrows(IllegalArgumentException.class, () -> operation.validate()); 35 | } 36 | 37 | @Test 38 | void whenValueNotSuppliedThenIllegalArgumentExcThrown() { 39 | final var operation = new MultiValueOperation("birthDate", IN, null); 40 | assertThrows(NullPointerException.class, () -> operation.validate()); 41 | } 42 | 43 | @Test 44 | void shouldDeserializeCorrectOperationType() throws JsonProcessingException { 45 | final var objectMapper = TestUtil.createObjectMapper(); 46 | var json = "{\"property\": \"customerId\",\"operator\": \"IN\",\"value\": ["+ 47 | "1,2,3,4,5]}"; 48 | final var operation = objectMapper.readValue(json, AbstractOperation.class); 49 | assertTrue(operation instanceof MultiValueOperation); 50 | assertEquals("customerId", operation.getProperty()); 51 | assertEquals(IN, operation.getOperator()); 52 | assertTrue(Arrays.equals(new Integer[]{1,2,3,4,5}, ((MultiValueOperation) operation).getValue())); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/dto/operation/NoValueOperationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.dto.operation; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.ozayduman.specificationbuilder.TestUtil; 6 | import com.github.ozayduman.specificationbuilder.dto.Operator; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.EnumSet; 10 | import java.util.List; 11 | 12 | import static com.github.ozayduman.specificationbuilder.dto.Operator.*; 13 | import static org.junit.jupiter.api.Assertions.*; 14 | 15 | class NoValueOperationTest { 16 | @Test 17 | void shouldAllowTheseOperators() { 18 | final var operation = new NoValueOperation("hasCar", TRUE); 19 | final var operators = operation.allowedOperators(); 20 | assertTrue(operators.containsAll(List.of(TRUE, FALSE, NULL, NOT_NULL))); 21 | } 22 | 23 | @Test 24 | void shouldNotHaveAOperand() { 25 | final var operation = new NoValueOperation("hasCar", TRUE); 26 | final var operands = operation.getOperands(); 27 | assertEquals(0, operands.length); 28 | } 29 | 30 | @Test 31 | void whenAllowedOperatorSuppliedThenOperationValidated() { 32 | final var operation = new NoValueOperation("hasCar", TRUE); 33 | assertDoesNotThrow(() -> operation.validate()); 34 | } 35 | 36 | @Test 37 | void whenNotAllowedOperatorSuppliedThenIllegalArgumentExceptionThrown() { 38 | final var operation = new NoValueOperation("hasCar", EQ); 39 | assertThrows(IllegalArgumentException.class, () -> operation.validate()); 40 | } 41 | 42 | @Test 43 | void shouldDeserializeToCorrectOperationType() throws JsonProcessingException { 44 | final var objectMapper = TestUtil.createObjectMapper(); 45 | var json = "{\"property\": \"phoneNumber\",\"operator\": \"NOT_NULL\"}"; 46 | final var operation = objectMapper.readValue(json, AbstractOperation.class); 47 | assertTrue(operation instanceof NoValueOperation); 48 | assertEquals("phoneNumber", operation.getProperty()); 49 | assertEquals(NOT_NULL, operation.getOperator()); 50 | assertEquals(0, operation.getOperands().length); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/dto/operation/RangeValueOperationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.dto.operation; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.ozayduman.specificationbuilder.TestUtil; 6 | import com.github.ozayduman.specificationbuilder.dto.Operator; 7 | import com.github.ozayduman.specificationbuilder.dto.RangeDTO; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.List; 11 | 12 | import static com.github.ozayduman.specificationbuilder.dto.Operator.BT; 13 | import static org.junit.jupiter.api.Assertions.*; 14 | 15 | class RangeValueOperationTest { 16 | @Test 17 | void shouldAllowTheseOperators() { 18 | final var operation = new RangeValueOperation(); 19 | assertTrue(operation.allowedOperators().containsAll(List.of(BT))); 20 | } 21 | 22 | @Test 23 | void whenAllowedOperatorSuppliedThenOperationValidated() { 24 | final var operation = new RangeValueOperation("numbers", BT, new RangeDTO(1, 5)); 25 | assertDoesNotThrow(() -> operation.validate()); 26 | } 27 | 28 | @Test 29 | void whenNotAllowedOperatorSuppliedThenIllegalArgumentExceptionThrown() { 30 | final var operation = new RangeValueOperation("numbers", Operator.IN, new RangeDTO(1, 5)); 31 | assertThrows(IllegalArgumentException.class, () -> operation.validate()); 32 | } 33 | 34 | @Test 35 | void whenNoValueSuppliedThenNullPointerExceptionThrown() { 36 | final var operation = new RangeValueOperation("numbers", BT, null); 37 | assertThrows(NullPointerException.class, () -> operation.validate()); 38 | } 39 | 40 | @Test 41 | void whenEmptyValueSuppliedThenNullPointerExceptionThrown() { 42 | final var operation = new RangeValueOperation("numbers", BT, new RangeDTO()); 43 | assertThrows(NullPointerException.class, () -> operation.validate()); 44 | } 45 | 46 | @Test 47 | void shouldDeserializeCorrectOperationType() throws JsonProcessingException { 48 | final var objectMapper = TestUtil.createObjectMapper(); 49 | var json = "{\"property\": \"age\",\"operator\": \"BT\",\"value\": {\"low\": 18,\"high\": 65}}"; 50 | final var operation = objectMapper.readValue(json, AbstractOperation.class); 51 | assertTrue(operation instanceof RangeValueOperation); 52 | assertEquals("age",operation.getProperty()); 53 | assertEquals(BT, operation.getOperator()); 54 | assertAll("range dto", 55 | () -> assertNotNull(operation.getOperands()), 56 | () -> assertEquals(2, operation.getOperands().length), 57 | () -> assertEquals(18, ((RangeDTO)((RangeValueOperation) operation).getValue()).getLow()), 58 | () -> assertEquals(65, ((RangeDTO)((RangeValueOperation) operation).getValue()).getHigh()) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/dto/operation/SingleValueOperationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.dto.operation; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.ozayduman.specificationbuilder.TestUtil; 6 | import com.github.ozayduman.specificationbuilder.dto.Operator; 7 | import lombok.val; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.EnumSet; 11 | import java.util.List; 12 | 13 | import static com.github.ozayduman.specificationbuilder.dto.Operator.*; 14 | import static com.github.ozayduman.specificationbuilder.dto.Operator.LE; 15 | import static org.junit.jupiter.api.Assertions.*; 16 | 17 | class SingleValueOperationTest { 18 | 19 | @Test 20 | void shouldAllowTheseOperators() { 21 | final var operation = new SingleValueOperation("name", Operator.EQ, "ozay"); 22 | final var operators = operation.allowedOperators(); 23 | operators.containsAll(List.of(EQ, NOT_EQ, GT, GE, LT, LE, LIKE, NOT_LIKE)); 24 | } 25 | 26 | @Test 27 | void whenAllowedOperationSuppliedThenOperationValidated() { 28 | final var operation = new SingleValueOperation("name", Operator.EQ, "ozay"); 29 | assertDoesNotThrow(() -> operation.validate()); 30 | } 31 | 32 | @Test 33 | void whenNotAllowedOperatorSuppliedThenIllegalArgumentExcThrown() { 34 | final var operation = new SingleValueOperation("name", Operator.BT, "ozay"); 35 | assertThrows(IllegalArgumentException.class, () -> operation.validate()); 36 | } 37 | 38 | @Test 39 | void whenLikeOperatorsWithNotAStringTypeGivenThenIllegalArgumentExcThrown() { 40 | val likeOperation = new SingleValueOperation("name", LIKE, Integer.valueOf(1)); 41 | val notLikeOperation = new SingleValueOperation("name", NOT_LIKE, Integer.valueOf(1)); 42 | assertAll( 43 | () -> assertThrows(IllegalArgumentException.class, likeOperation::validate), 44 | () -> assertThrows(IllegalArgumentException.class, notLikeOperation::validate) 45 | ); 46 | } 47 | 48 | @Test 49 | void whenLikeOperatorsWithAStringTypeGivenThenOperationValidated() { 50 | val likeOperation = new SingleValueOperation("name", LIKE, "özay"); 51 | val notLikeOperation = new SingleValueOperation("name", NOT_LIKE, "özay"); 52 | assertDoesNotThrow(likeOperation::validate); 53 | assertDoesNotThrow(notLikeOperation::validate); 54 | } 55 | 56 | @Test 57 | void shouldDeserializeCorrectOperationType() throws JsonProcessingException { 58 | final var objectMapper = TestUtil.createObjectMapper(); 59 | var json = "{\"property\": \"name\",\"operator\": \"EQ\",\"value\": \"Alice\"}"; 60 | final var operation = objectMapper.readValue(json, AbstractOperation.class); 61 | assertTrue(operation instanceof SingleValueOperation); 62 | assertEquals("name", operation.getProperty()); 63 | assertEquals(EQ, operation.getOperator()); 64 | assertEquals("Alice", ((SingleValueOperation) operation).getValue()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/entity/Employee.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.entity; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.*; 6 | import java.time.LocalDate; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | @Entity 11 | @Data 12 | @RequiredArgsConstructor 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class Employee { 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.AUTO) 18 | private Long id; 19 | 20 | @NonNull 21 | private String name; 22 | @NonNull 23 | private String surname; 24 | @NonNull 25 | private String email; 26 | @NonNull 27 | private LocalDate birthDate; 28 | 29 | @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) 30 | private SocialSecurity socialSecurity; 31 | 32 | @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) 33 | private List phones = new ArrayList<>(); 34 | 35 | public void addPhone(Phone phone) { 36 | phone.setEmployee(this); 37 | phones.add(phone); 38 | } 39 | 40 | public static Employee of(String name, String surname, String email, 41 | LocalDate birthDate, SocialSecurity socialSecurity) { 42 | val employee = new Employee(name, surname, email, birthDate); 43 | employee.setSocialSecurity(socialSecurity); 44 | return employee; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/entity/Phone.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.persistence.*; 9 | 10 | @Entity 11 | @Data 12 | @Builder 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class Phone { 16 | @Id 17 | @GeneratedValue 18 | private long id; 19 | private PhoneType phoneType; 20 | private String number; 21 | 22 | @ManyToOne(fetch = FetchType.LAZY) 23 | @JoinColumn(name = "fk_employee_id") 24 | private Employee employee; 25 | 26 | @Override 27 | public String toString() { 28 | return "Phone{" + 29 | "phoneType=" + phoneType + 30 | ", number='" + number + 31 | '}'; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/entity/PhoneType.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.entity; 2 | 3 | public enum PhoneType { 4 | HOME,BUSSINES; 5 | } 6 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/entity/SocialSecurity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.entity; 34 | 35 | import lombok.Data; 36 | import lombok.NoArgsConstructor; 37 | import lombok.NonNull; 38 | import lombok.RequiredArgsConstructor; 39 | 40 | import javax.persistence.Entity; 41 | import javax.persistence.GeneratedValue; 42 | import javax.persistence.GenerationType; 43 | import javax.persistence.Id; 44 | 45 | @Entity 46 | @Data 47 | @NoArgsConstructor 48 | @RequiredArgsConstructor 49 | public class SocialSecurity { 50 | @Id 51 | @GeneratedValue(strategy = GenerationType.AUTO) 52 | private Long id; 53 | 54 | @NonNull 55 | private SocialSecurityType socialSecurityType; 56 | 57 | @NonNull 58 | private String explanation; 59 | 60 | public static SocialSecurity of(SocialSecurityType type, String explanation) { 61 | return new SocialSecurity(type, explanation); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/entity/SocialSecurityType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * _____ _ __ _ _ _ 3 | * / ___| (_)/ _(_) | | (_) 4 | * \ `--. _ __ ___ ___ _| |_ _ ___ __ _ __ _| |_ _ ___ _ __ 5 | * `--. \ '_ \ / _ \/ __| | _| |/ __/ _` |/ _` | __| |/ _ \| '_ \ 6 | * /\__/ / |_) | __/ (__| | | | | (_| (_| | (_| | |_| | (_) | | | | 7 | * \____/| .__/ \___|\___|_|_| |_|\___\__,_|\__, |\__|_|\___/|_| |_| 8 | * | | __/ | 9 | * |_| |___/ 10 | * ______ _ _ _ 11 | * | ___ \ (_) | | | 12 | * | |_/ /_ _ _| | __| | ___ _ __ 13 | * | ___ \ | | | | |/ _` |/ _ \ '__| 14 | * | |_/ / |_| | | | (_| | __/ | 15 | * \____/ \__,_|_|_|\__,_|\___|_| 16 | * 17 | * Copyright 2021 Specification Builder, https://github.com/ozayduman/specification-builder 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | package com.github.ozayduman.specificationbuilder.entity; 34 | 35 | public enum SocialSecurityType { 36 | A, B; 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/mapper/EmployeeMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.mapper; 2 | 3 | import com.github.ozayduman.specificationbuilder.dto.EmployeeResponseDTO; 4 | import com.github.ozayduman.specificationbuilder.entity.Employee; 5 | import org.mapstruct.InheritInverseConfiguration; 6 | import org.mapstruct.Mapper; 7 | import org.mapstruct.Mapping; 8 | import org.mapstruct.Mappings; 9 | import org.mapstruct.factory.Mappers; 10 | 11 | @Mapper 12 | public interface EmployeeMapper { 13 | EmployeeMapper INSTANCE = Mappers.getMapper(EmployeeMapper.class); 14 | 15 | @Mappings({ 16 | @Mapping(target = "name" ,source = "name"), 17 | @Mapping(target = "lastName", source = "surname"), 18 | @Mapping(target = "email", source = "email"), 19 | @Mapping(target = "birthDate", source = "birtDate") 20 | }) 21 | EmployeeResponseDTO toDTO(Employee employee); 22 | 23 | @InheritInverseConfiguration 24 | Employee toEntity(EmployeeResponseDTO dto); 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/github/ozayduman/specificationbuilder/repository/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.ozayduman.specificationbuilder.repository; 2 | 3 | import com.github.ozayduman.specificationbuilder.entity.Employee; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface EmployeeRepository extends JpaRepository, JpaSpecificationExecutor { 12 | } 13 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:mem:testdb 2 | spring.datasource.driverClassName=org.h2.Driver 3 | spring.datasource.username=sa 4 | spring.datasource.password=password 5 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 6 | 7 | spring.jpa.hibernate.ddl-auto=create 8 | #logging.level.org.hibernate.SQL=DEBUG 9 | #logging.level.org.hibernate.type.descriptor.sql=TRACE 10 | #logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 11 | --------------------------------------------------------------------------------