├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── img └── property-selector.png ├── main └── java │ └── com │ └── jaxio │ └── jpa │ └── querybyexample │ ├── ByExampleUtil.java │ ├── ByFullTextUtil.java │ ├── ByNamedQueryUtil.java │ ├── ByPatternUtil.java │ ├── ByPropertySelectorUtil.java │ ├── ByRangeUtil.java │ ├── DefaultLuceneQueryBuilder.java │ ├── EntityGraphLoader.java │ ├── GenericRepository.java │ ├── HibernateSearchUtil.java │ ├── Identifiable.java │ ├── JpaUniqueUtil.java │ ├── JpaUtil.java │ ├── LabelizedEnum.java │ ├── LuceneQueryBuilder.java │ ├── MetamodelUtil.java │ ├── OrderBy.java │ ├── OrderByDirection.java │ ├── OrderByUtil.java │ ├── PathHolder.java │ ├── PropertySelector.java │ ├── Range.java │ ├── RepositoryLocator.java │ ├── SearchMode.java │ ├── SearchParameters.java │ └── TermSelector.java └── test ├── java └── demo │ ├── Account.java │ ├── AccountQueryByExampleTest.java │ ├── AccountRepository.java │ ├── Account_.java │ ├── Address.java │ ├── Address_.java │ ├── IdentifiableHashBuilder.java │ └── JpaConfiguration.java └── resources ├── 01-create.sql ├── META-INF └── persistence.xml ├── applicationContext.xml └── hibernate.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | target/ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | -------------------------------------------------------------------------------- /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 | ## JPA Query by Example framework 2 | 3 | [![Build Status](https://travis-ci.org/jaxio/jpa-query-by-example.svg?branch=master)](https://travis-ci.org/jaxio/jpa-query-by-example) 4 | 5 | Query By Example for JPA is originally inspired from [Hibernate Example criterion](https://docs.jboss.org/hibernate/orm/3.6/reference/en-US/html/querycriteria.html#querycriteria-examples 6 | ). But since Hibernate's Example is not part of JPA 2, we have created our own API, using JPA 2 only. 7 | 8 | 9 | ## How To Use QBE 10 | 11 | We do not cover here QBE implementation details, instead we explain how to use the Query By Example API. 12 | 13 | JPA Query by Example is available on Maven central repository: 14 | 15 | ```xml 16 | 17 | com.jaxio 18 | jpa-querybyexample 19 | 1.0.1 20 | 21 | ``` 22 | 23 | #### Resources 24 | 25 | * Take a look directly at the [QBE junit tests](https://github.com/jaxio/jpa-query-by-example/blob/master/src/test/java/demo), they are almost self-explanatory. 26 | * Use Celerio to generate an advanced CRUD application that leverages this QBE API. See [Celerio](http://www.jaxio.com/documentation/celerio/installation.html) 27 | * [Watch a demo of an application generated by Celerio](https://www.facebook.com/video/video.php?v=524162864265905¬if_t=video_processed) 28 | 29 | 30 | ### Simple Query By Example 31 | 32 | In its simplest form, Query By Example allows you to construct a query from a given entity instance. 33 | 34 | Let's assume we have an [Account entity](https://github.com/jaxio/jpa-query-by-example/blob/master/src/test/java/demo/Account.java) 35 | having a `lastName` property and that we want to query all accounts whose last name matches 'Jagger'. 36 | 37 | Using QBE, constructing the query is as simple as setting the lastName...: 38 | 39 | ```java 40 | Account example = new Account(); 41 | example.setLastName("Jagger"); 42 | List result = accountRepository.find(example); 43 | ``` 44 | 45 | At the SQL level, the resulting query looks like this: 46 | 47 | ```sql 48 | select 49 | -- skip other fields for clarity 50 | account0_.LAST_NAME as LAST9_3_, 51 | from 52 | Account account0_ 53 | where 54 | account0_.LAST_NAME=? 55 | ``` 56 | 57 | The [AccountRepository](https://github.com/jaxio/jpa-query-by-example/blob/master/src/test/java/demo/AccountRepository.java) 58 | extends a [GenericRepository](https://github.com/jaxio/jpa-query-by-example/blob/master/src/main/java/com/jaxio/jpa/querybyexample/GenericRepository.java) 59 | 60 | #### Case sensitivity, order by 61 | 62 | The first query above involves a String. Let's change it to make it case insensitive. 63 | 64 | Our `Account` entity does not carry case sensitivity meta information. For this reason, we require some extra parameters 65 | for case sensitivity, but also ordering, etc. 66 | The number of parameters can grow quickly, so we have grouped them in the 67 | [SearchParameters](https://github.com/jaxio/jpa-query-by-example/blob/master/src/main/java/com/jaxio/jpa/querybyexample/SearchParameters.java) class 68 | which can be passed as a parameter to the accountRepository's methods. 69 | 70 | Let's make the first query above `case insensitive` and let's add an `ORDER BY`. 71 | 72 | ```java 73 | Account example = new Account(); 74 | example.setLastName("Jagger"); 75 | SearchParameters sp = new SearchParameters().caseSensitive().orderBy(OrderByDirection.ASC, Account_.lastName); 76 | List result = accountRepository.find(example, sp); 77 | ``` 78 | 79 | Note the usage of the 80 | [Account_](https://github.com/jaxio/jpa-query-by-example/blob/master/src/test/java/demo/Account_.java)* 81 | static metamodel, which helps you to keep your query related Java code strongly typed. 82 | 83 | At the SQL level, the resulting FROM clause now looks like this: 84 | 85 | ```sql 86 | from 87 | ACCOUNT account0_ 88 | where 89 | lower(account0_.LAST_NAME)=? 90 | order by 91 | account0_.LAST_NAME asc 92 | ``` 93 | 94 | #### Pagination 95 | 96 | In most web application we need to paginate the query results in order to save resources. In the query below, we retrieve only 97 | the 3rd page (we assume a page lists 25 rows). The first result is the 50th element and we retrieve at most 25 elements. 98 | 99 | ```java 100 | Account example = new Account(); 101 | example.setLastName("Jagger"); 102 | SearchParameters sp = new SearchParameters().orderBy(OrderByDirection.ASC, Account_.lastName) // 103 | .first(50).maxResults(25); 104 | List result = accountRepository.find(example, sp); 105 | ``` 106 | 107 | At the SQL level, the resulting FROM clause now looks like this (we use H2 database): 108 | 109 | ```sql 110 | from 111 | ACCOUNT account0_ 112 | where 113 | account0_.LAST_NAME=? 114 | order by 115 | account0_.LAST_NAME asc limit ? offset ? 116 | ``` 117 | 118 | #### LIKE and String 119 | 120 | For strings, you can globally control whether a `LIKE` should be used and where the `%` wildcard should be placed. For example, adding : 121 | 122 | ```java 123 | example.setLastName("Jag"); 124 | SearchParameters sp = new SearchParameters().startingLike(); 125 | ``` 126 | 127 | to our example above would result in 128 | 129 | ```sql 130 | account0_.LAST_NAME LIKE 'Jag%' 131 | ``` 132 | 133 | #### Multiple criteria 134 | 135 | Until now, we have worked only with one property, lastName, but we can set other properties, for example: 136 | 137 | ```java 138 | Account example = new Account(); 139 | example.setLastName("Jag"); 140 | example.setBirthDate(new Date()); 141 | SearchParameters sp = new SearchParameters().orderBy(OrderByDirection.ASC, Account_.lastName).startingLike(); 142 | List result = accountRepository.find(example, sp); 143 | ``` 144 | 145 | By default, the FROM clause uses a `AND` predicate. 146 | 147 | ```sql 148 | from 149 | ACCOUNT account0_ 150 | where 151 | account0_.BIRTH_DATE=? 152 | and ( 153 | account0_.LAST_NAME like ? 154 | ) 155 | order by 156 | account0_.LAST_NAME asc 157 | ``` 158 | 159 | To use instead `OR`, use the `.orMode()`, as follow: 160 | 161 | ```java 162 | SearchParameters sp = new SearchParameters().orMode().orderBy(OrderByDirection.ASC, Account_.lastName).startingLike(); 163 | ``` 164 | 165 | And this time we get: 166 | 167 | ```sql 168 | where 169 | account0_.LAST_NAME like ? 170 | or account0_.BIRTH_DATE=? 171 | order by 172 | account0_.LAST_NAME asc 173 | ``` 174 | 175 | #### Is that all ? 176 | 177 | Not really, we have just scratched the surface. For the moment, we have covered only rather simple queries. 178 | While simplicity is key, it is often not sufficient. What about date or number range queries ? What about associated entities ? etc. 179 | 180 | ### Beyond Query By Example 181 | 182 | #### Mixing Query by Example and Range Query. 183 | 184 | Now, let's imagine that you also want to restrict the query above to all accounts having their date of birth between 1940 and 1945 included. 185 | Of course, the entity does not have the appropriate property (from & to). 186 | For this reason, we introduce an additional 187 | [Range](https://github.com/jaxio/jpa-query-by-example/blob/master/src/main/java/com/jaxio/jpa/querybyexample/Range.java) 188 | parameter. 189 | 190 | Here is an example: 191 | 192 | ```java 193 | Account example = new Account(); 194 | example.setLastName("Jagger"); 195 | 196 | Calendar from = Calendar.getInstance(); 197 | from.set(1940, 0, 1); 198 | 199 | Calendar to = Calendar.getInstance(); 200 | to.set(1945, 11, 31); 201 | 202 | Range birthDateRange = Range.newRange(Account_.birthDate); 203 | birthDateRange.from(from.getTime()).to(to.getTime()); 204 | 205 | SearchParameters sp = new SearchParameters().range(birthDateRange); 206 | List result = accountRepository.find(example, sp); 207 | ``` 208 | 209 | Note that you can add ranges of any type: Integer, Long, LocalDate (joda time), BigDecimal, etc... 210 | 211 | This codes leads in fine to following `FROM` clause: 212 | 213 | ```sql 214 | from 215 | ACCOUNT account0_ 216 | where 217 | ( 218 | account0_.BIRTH_DATE between ? and ? 219 | ) 220 | and account0_.LAST_NAME=? 221 | ``` 222 | 223 | Here is a variation of the same example (depends on need, taste and color :-): 224 | 225 | ```java 226 | DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); 227 | Date from = dateFormat.parse("1920-12-01"); 228 | Date to = dateFormat.parse("1974-12-01"); 229 | 230 | SearchParameters sp = new SearchParameters().range(from, to, Account_.birthDate); 231 | List accountList = accountRepository.find(sp); 232 | ``` 233 | 234 | #### Query all string properties in a OR clause 235 | 236 | To find all entities having at least one of their String property matching a given value, use the `searchPattern` method. 237 | 238 | Here is an example: 239 | 240 | ```java 241 | SearchParameters sp = new SearchParameters().searchMode(SearchMode.STARTING_LIKE).searchPattern("Jag"); 242 | List result = accountRepository.find(sp); 243 | ``` 244 | 245 | The FROM clause now includes all string columns: 246 | 247 | ```sql 248 | from 249 | ACCOUNT account0_ 250 | where 251 | or account0_.LAST_NAME like ? 252 | or account0_.USERNAME like ? 253 | ``` 254 | 255 | #### Property Selector 256 | 257 | In order to construct a `OR` clause for a given property we use the `PropertySelector` class. 258 | 259 | Here is an example: 260 | 261 | ```java 262 | PropertySelector lastNameSelector = PropertySelector.newPropertySelector(Account_.lastName); 263 | lastNameSelector.setSelected(Arrays.asList("Jagger", "Richards", "Jones", "Watts", "taylor", "Wyman", "Wood")); 264 | 265 | SearchParameters sp = new SearchParameters().property(lastNameSelector); 266 | 267 | List result = accountRepository.find(sp); 268 | ``` 269 | 270 | Here is the corresponding FROM clause: 271 | 272 | ```sql 273 | from 274 | ACCOUNT account0_ 275 | where 276 | account0_.LAST_NAME='Jagger' 277 | or account0_.LAST_NAME='Richards' 278 | or account0_.LAST_NAME='Jones' 279 | or account0_.LAST_NAME='Watts' 280 | or account0_.LAST_NAME='Taylor' 281 | or account0_.LAST_NAME='Wyman' 282 | or account0_.LAST_NAME='Wood' 283 | ``` 284 | 285 | Note that if you use JSF2 with PrimeFaces, you can directly pass a `PropertySelector` to a multiple autoComplete component's value property. 286 | This way, the autoComplete component fills the PropertySelector. Here is how: 287 | 288 | ```xml 289 | 290 | ``` 291 | 292 | Here is a snapshot: 293 | 294 | ![property selector](https://github.com/jaxio/jpa-query-by-example/blob/master/src/img/property-selector.png) 295 | 296 | PrimeFaces uses the `setSelected(List selection)` method to fill the lastNameSelector. 297 | 298 | #### Mix it all 299 | 300 | Remember, you can mix all the example we have seen so far. 301 | You can have in a single query having multiple ranges, multiple property selector, multiple properties set on the example entity, etc. 302 | 303 | This gives you great power ;-) 304 | 305 | #### Query By Example on association 306 | 307 | The `Account` entity has a `@ManyToOne` association with the `Address` entity. 308 | 309 | Here is how we can retrieve all accounts pointing to an Address having its `city` property set to "Paris": 310 | 311 | ```java 312 | Account example = new Account(); 313 | example.setHomeAddress(new Address()); 314 | example.getHomeAddress().setCity("Paris"); 315 | List result = accountRepository.find(example); 316 | Assert.assertThat(result.size(), is(2)); 317 | ``` 318 | 319 | The FROM clause uses a JOIN: 320 | 321 | ```sql 322 | from 323 | ACCOUNT account0_ cross 324 | join 325 | ADDRESS address1_ 326 | where 327 | account0_.ADDRESS_ID=address1_.ID 328 | and address1_.CITY='Paris' 329 | ``` 330 | 331 | Enjoy! 332 | 333 | 334 | ## License 335 | 336 | The JPA Query By Example Framework is released under version 2.0 of the [Apache License][]. 337 | 338 | [Apache License]: http://www.apache.org/licenses/LICENSE-2.0 339 | 340 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | 3.1.1 7 | 8 | 9 | com.jaxio 10 | jpa-querybyexample 11 | jar 12 | 1.0.2-SNAPSHOT 13 | 14 | JPA Query by Example 15 | https://github.com/jaxio/jpa-query-by-example 16 | This API allows you to construct Query By Example using the JPA 2 Criteria API. 17 | 18 | 19 | 20 | nromanetti 21 | Nicolas Romanetti 22 | romanetti@gmail.com 23 | 24 | 25 | framiere 26 | Florent Ramiere 27 | florent@ramiere.com 28 | 29 | 30 | jeanlouisboudart 31 | Jean-Louis Boudart 32 | jeanlouis.boudart@gmail.com 33 | 34 | 35 | 36 | 37 | JAXIO 38 | http://www.jaxio.com 39 | 40 | 41 | 42 | 43 | Apache License 2.0 44 | http://www.apache.org/licenses/LICENSE-2.0.html 45 | repo 46 | 47 | 48 | 49 | 50 | scm:git:git://github.com/jaxio/jpa-query-by-example.git 51 | scm:git:git@github.com:jaxio/jpa-query-by-example.git 52 | http://github.com/jaxio/jpa-query-by-example/ 53 | HEAD 54 | 55 | 56 | 57 | github 58 | http://github.com/jaxio/jpa-query-by-example/issues#issue/ 59 | 60 | 61 | 62 | 63 | 64 | 65 | UTF-8 66 | UTF-8 67 | 68 | 69 | 70 | 5.0.2.Final 71 | 4.2.2.RELEASE 72 | 4.0.2.RELEASE 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | javax.inject 81 | javax.inject 82 | 1 83 | 84 | 85 | 86 | 87 | 88 | 89 | org.hibernate.javax.persistence 90 | hibernate-jpa-2.1-api 91 | 1.0.0.Final 92 | 93 | 94 | org.hibernate 95 | hibernate-core 96 | ${hibernate.version} 97 | 98 | 99 | org.hibernate 100 | hibernate-entitymanager 101 | ${hibernate.version} 102 | 103 | 104 | org.hibernate 105 | hibernate-java8 106 | ${hibernate.version} 107 | 108 | 109 | cglib 110 | cglib 111 | 3.1 112 | runtime 113 | 114 | 115 | 116 | 117 | 118 | org.hibernate 119 | hibernate-search 120 | 5.5.0.Final 121 | 122 | 123 | lucene-kuromoji 124 | org.apache.lucene 125 | 126 | 127 | 128 | 129 | org.apache.solr 130 | solr-common 131 | 1.3.0 132 | 133 | 134 | org.apache.solr 135 | solr-core 136 | 5.3.1 137 | 138 | 139 | org.apache.tika 140 | tika-parsers 141 | 1.11 142 | 143 | 144 | commons-logging 145 | commons-logging 146 | 147 | 148 | 149 | 150 | org.codehaus.jackson 151 | jackson-mapper-asl 152 | 1.9.13 153 | 154 | 155 | 156 | 157 | 158 | org.slf4j 159 | slf4j-api 160 | 1.7.12 161 | 162 | 163 | org.slf4j 164 | log4j-over-slf4j 165 | 1.7.12 166 | 167 | 168 | org.slf4j 169 | jcl-over-slf4j 170 | 1.7.12 171 | 172 | 173 | ch.qos.logback 174 | logback-classic 175 | 1.1.3 176 | 177 | 178 | 179 | 180 | 181 | com.google.guava 182 | guava 183 | 18.0 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | commons-beanutils 197 | commons-beanutils 198 | 1.9.2 199 | 200 | 201 | commons-logging 202 | commons-logging 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | org.springframework 211 | spring-context 212 | ${spring.version} 213 | 214 | 215 | commons-logging 216 | commons-logging 217 | 218 | 219 | 220 | 221 | org.springframework 222 | spring-expression 223 | ${spring.version} 224 | 225 | 226 | org.springframework 227 | spring-jdbc 228 | ${spring.version} 229 | 230 | 231 | org.springframework 232 | spring-orm 233 | ${spring.version} 234 | 235 | 236 | org.springframework 237 | spring-aspects 238 | ${spring.version} 239 | 240 | 241 | org.springframework 242 | spring-aop 243 | ${spring.version} 244 | 245 | 246 | org.aspectj 247 | aspectjrt 248 | 1.8.7 249 | 250 | 251 | org.aspectj 252 | aspectjweaver 253 | 1.8.7 254 | 255 | 256 | 257 | com.h2database 258 | h2 259 | 1.4.190 260 | test 261 | 262 | 263 | junit 264 | junit 265 | 4.12 266 | test 267 | 268 | 269 | org.springframework 270 | spring-test 271 | 4.2.2.RELEASE 272 | test 273 | 274 | 275 | 276 | 277 | 278 | 279 | org.apache.maven.plugins 280 | maven-deploy-plugin 281 | 2.8.1 282 | 283 | 284 | org.apache.maven.plugins 285 | maven-compiler-plugin 286 | 2.0.2 287 | 288 | 1.8 289 | 1.8 290 | 1.8 291 | UTF-8 292 | 293 | 294 | 295 | 296 | org.apache.maven.plugins 297 | maven-release-plugin 298 | 2.5.2 299 | 300 | release 301 | true 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | sonatype-nexus-snapshots 310 | Sonatype Nexus Snapshots 311 | http://oss.sonatype.org/content/repositories/snapshots 312 | 313 | 314 | sonatype-nexus-staging 315 | Nexus Release Repository 316 | http://oss.sonatype.org/service/local/staging/deploy/maven2/ 317 | 318 | 319 | 320 | 321 | 322 | release 323 | 324 | 325 | 326 | maven-gpg-plugin 327 | 1.6 328 | 329 | 330 | sign-artifacts 331 | verify 332 | 333 | sign 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | -------------------------------------------------------------------------------- /src/img/property-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaxio/jpa-query-by-example/7ee8503a5823ae5504d31fbdac24e0c7bd608828/src/img/property-selector.png -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/ByExampleUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import javax.inject.Inject; 22 | import javax.inject.Named; 23 | import javax.inject.Singleton; 24 | import javax.persistence.EntityManager; 25 | import javax.persistence.PersistenceContext; 26 | import javax.persistence.criteria.*; 27 | import javax.persistence.metamodel.ManagedType; 28 | import javax.persistence.metamodel.PluralAttribute; 29 | import javax.persistence.metamodel.SingularAttribute; 30 | import java.util.List; 31 | 32 | import static com.google.common.collect.Lists.newArrayList; 33 | import static java.util.Collections.emptyList; 34 | import static javax.persistence.metamodel.Attribute.PersistentAttributeType.*; 35 | import static org.apache.commons.lang.StringUtils.isNotEmpty; 36 | 37 | /** 38 | * Helper to create predicate by example. It processes associated entities (1 level deep). 39 | */ 40 | @Named 41 | @Singleton 42 | public class ByExampleUtil { 43 | private static final Logger log = LoggerFactory.getLogger(ByExampleUtil.class); 44 | 45 | @Inject 46 | private JpaUtil jpaUtil; 47 | @PersistenceContext 48 | private EntityManager em; 49 | 50 | public > Predicate byExampleOnEntity(Root rootPath, T entityValue, CriteriaBuilder builder, SearchParameters sp) { 51 | if (entityValue == null) { 52 | return null; 53 | } 54 | 55 | Class type = rootPath.getModel().getBindableJavaType(); 56 | ManagedType mt = em.getMetamodel().entity(type); 57 | 58 | List predicates = newArrayList(); 59 | predicates.addAll(byExample(mt, rootPath, entityValue, sp, builder)); 60 | predicates.addAll(byExampleOnCompositePk(rootPath, entityValue, sp, builder)); 61 | predicates.addAll(byExampleOnXToOne(mt, rootPath, entityValue, sp, builder)); // 1 level deep only 62 | predicates.addAll(byExampleOnXToMany(mt, rootPath, entityValue, sp, builder)); 63 | return jpaUtil.concatPredicate(sp, builder, predicates); 64 | } 65 | 66 | protected > List byExampleOnCompositePk(Root root, T entity, SearchParameters sp, CriteriaBuilder builder) { 67 | String compositePropertyName = jpaUtil.compositePkPropertyName(entity); 68 | if (compositePropertyName == null) { 69 | return emptyList(); 70 | } else { 71 | return newArrayList(byExampleOnEmbeddable(root.get(compositePropertyName), entity.getId(), sp, builder)); 72 | } 73 | } 74 | 75 | public Predicate byExampleOnEmbeddable(Path embeddablePath, E embeddableValue, SearchParameters sp, CriteriaBuilder builder) { 76 | if (embeddableValue == null) { 77 | return null; 78 | } 79 | 80 | Class type = embeddablePath.getModel().getBindableJavaType(); 81 | ManagedType mt = em.getMetamodel().embeddable(type); // note: calling .managedType() does not work 82 | 83 | return jpaUtil.andPredicate(builder, byExample(mt, embeddablePath, embeddableValue, sp, builder)); 84 | } 85 | 86 | /* 87 | * Add a predicate for each simple property whose value is not null. 88 | */ 89 | public List byExample(ManagedType mt, Path mtPath, T mtValue, SearchParameters sp, CriteriaBuilder builder) { 90 | List predicates = newArrayList(); 91 | for (SingularAttribute attr : mt.getSingularAttributes()) { 92 | if (attr.getPersistentAttributeType() == MANY_TO_ONE // 93 | || attr.getPersistentAttributeType() == ONE_TO_ONE // 94 | || attr.getPersistentAttributeType() == EMBEDDED) { 95 | continue; 96 | } 97 | 98 | Object attrValue = jpaUtil.getValue(mtValue, attr); 99 | if (attrValue != null) { 100 | if (attr.getJavaType() == String.class) { 101 | if (isNotEmpty((String) attrValue)) { 102 | predicates.add(jpaUtil.stringPredicate(mtPath.get(jpaUtil.stringAttribute(mt, attr)), attrValue, sp, builder)); 103 | } 104 | } else { 105 | predicates.add(builder.equal(mtPath.get(jpaUtil.attribute(mt, attr)), attrValue)); 106 | } 107 | } 108 | } 109 | return predicates; 110 | } 111 | 112 | /* 113 | * Invoke byExample method for each not null x-to-one association when their pk is not set. This allows you to search entities based on an associated 114 | * entity's properties value. 115 | */ 116 | @SuppressWarnings("unchecked") 117 | public , M2O extends Identifiable> List byExampleOnXToOne(ManagedType mt, Root mtPath, T mtValue, 118 | SearchParameters sp, CriteriaBuilder builder) { 119 | List predicates = newArrayList(); 120 | for (SingularAttribute attr : mt.getSingularAttributes()) { 121 | if (attr.getPersistentAttributeType() == MANY_TO_ONE || attr.getPersistentAttributeType() == ONE_TO_ONE) { 122 | M2O m2oValue = (M2O) jpaUtil.getValue(mtValue, mt.getAttribute(attr.getName())); 123 | Class m2oType = (Class) attr.getBindableJavaType(); 124 | Path m2oPath = (Path) mtPath.get(attr); 125 | ManagedType m2oMt = em.getMetamodel().entity(m2oType); 126 | if (m2oValue != null) { 127 | if (m2oValue.isIdSet()) { // we have an id, let's restrict only on this field 128 | predicates.add(builder.equal(m2oPath.get("id"), m2oValue.getId())); 129 | } else { 130 | predicates.addAll(byExample(m2oMt, m2oPath, m2oValue, sp, builder)); 131 | } 132 | } 133 | } 134 | } 135 | return predicates; 136 | } 137 | 138 | /* 139 | * Construct a join predicate on collection (eg many to many, List) 140 | */ 141 | public List byExampleOnXToMany(ManagedType mt, Root mtPath, T mtValue, SearchParameters sp, CriteriaBuilder builder) { 142 | List predicates = newArrayList(); 143 | for (PluralAttribute pa : mt.getPluralAttributes()) { 144 | if (pa.getCollectionType() == PluralAttribute.CollectionType.LIST) { 145 | List values = (List) jpaUtil.getValue(mtValue, mt.getAttribute(pa.getName())); 146 | if (values != null && !values.isEmpty()) { 147 | if (sp.getUseAndInXToMany()) { 148 | if (values.size() > 3) { 149 | log.warn("Please note that using AND restriction on an Many to Many relationship requires as many joins as values"); 150 | } 151 | for (Object value : values) { 152 | ListJoin join = mtPath.join(mt.getList(pa.getName())); 153 | predicates.add(join.in(value)); 154 | } 155 | } else { 156 | ListJoin join = mtPath.join(mt.getList(pa.getName())); 157 | predicates.add(join.in(values)); 158 | } 159 | } 160 | } 161 | } 162 | return predicates; 163 | } 164 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/ByFullTextUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import javax.inject.Inject; 19 | import javax.inject.Named; 20 | import javax.inject.Singleton; 21 | import javax.persistence.Embeddable; 22 | import javax.persistence.EntityManager; 23 | import javax.persistence.PersistenceContext; 24 | import javax.persistence.criteria.CriteriaBuilder; 25 | import javax.persistence.criteria.Path; 26 | import javax.persistence.criteria.Predicate; 27 | import javax.persistence.criteria.Root; 28 | import javax.persistence.metamodel.ManagedType; 29 | import javax.persistence.metamodel.SingularAttribute; 30 | import java.io.Serializable; 31 | import java.util.List; 32 | 33 | import static com.google.common.collect.Lists.newArrayList; 34 | import static java.util.Collections.emptyList; 35 | 36 | /** 37 | * @author Nicolas Romanetti 38 | * @author Florent Ramière 39 | * @author Sébastien Péralta 40 | * @author Jean-Louis Boudart 41 | */ 42 | @Named 43 | @Singleton 44 | public class ByFullTextUtil { 45 | @PersistenceContext 46 | private EntityManager em; 47 | 48 | @Inject 49 | protected HibernateSearchUtil hibernateSearchUtil; 50 | @Inject 51 | private JpaUtil jpaUtil; 52 | 53 | public > Predicate byFullText(Root root, CriteriaBuilder builder, SearchParameters sp, T entity, 54 | List> indexedAttributes) { 55 | if (!hasNonEmptyTerms(sp)) { 56 | return null; 57 | } 58 | 59 | if (jpaUtil.hasSimplePk(entity)) { 60 | return onSimplePrimaryKey(root, builder, sp, indexedAttributes); 61 | } else { 62 | return onCompositePrimaryKeys(root, builder, sp, indexedAttributes); 63 | } 64 | } 65 | 66 | private boolean hasNonEmptyTerms(SearchParameters sp) { 67 | for (TermSelector termSelector : sp.getTerms()) { 68 | if (termSelector.isNotEmpty()) { 69 | return true; 70 | } 71 | } 72 | return false; 73 | } 74 | 75 | private > Predicate onCompositePrimaryKeys(Root root, CriteriaBuilder builder, SearchParameters sp, 76 | List> properties) { 77 | List found = hibernateSearchUtil.find(root.getJavaType(), sp, properties); 78 | if (found == null) { 79 | return null; 80 | } else if (found.isEmpty()) { 81 | return builder.disjunction(); 82 | } 83 | 84 | List predicates = newArrayList(); 85 | for (T t : found) { 86 | predicates.add(byExampleOnEntity(root, t, sp, builder)); 87 | } 88 | return jpaUtil.concatPredicate(sp, builder, jpaUtil.orPredicate(builder, predicates)); 89 | } 90 | 91 | private Predicate onSimplePrimaryKey(Root root, CriteriaBuilder builder, SearchParameters sp, List> properties) { 92 | List ids = hibernateSearchUtil.findId(root.getJavaType(), sp, properties); 93 | if (ids == null) { 94 | return null; 95 | } else if (ids.isEmpty()) { 96 | return builder.disjunction(); 97 | } 98 | 99 | return jpaUtil.concatPredicate(sp, builder, root.get("id").in(ids)); 100 | } 101 | 102 | public > Predicate byExampleOnEntity(Root rootPath, T entityValue, SearchParameters sp, CriteriaBuilder builder) { 103 | if (entityValue == null) { 104 | return null; 105 | } 106 | 107 | Class type = rootPath.getModel().getBindableJavaType(); 108 | ManagedType mt = em.getMetamodel().entity(type); 109 | 110 | List predicates = newArrayList(); 111 | predicates.addAll(byExample(mt, rootPath, entityValue, sp, builder)); 112 | predicates.addAll(byExampleOnCompositePk(rootPath, entityValue, sp, builder)); 113 | return jpaUtil.orPredicate(builder, predicates); 114 | } 115 | 116 | protected > List byExampleOnCompositePk(Root root, T entity, SearchParameters sp, CriteriaBuilder builder) { 117 | String compositePropertyName = jpaUtil.compositePkPropertyName(entity); 118 | if (compositePropertyName == null) { 119 | return emptyList(); 120 | } else { 121 | return newArrayList(byExampleOnEmbeddable(root.get(compositePropertyName), entity.getId(), sp, builder)); 122 | } 123 | } 124 | 125 | public Predicate byExampleOnEmbeddable(Path embeddablePath, E embeddableValue, SearchParameters sp, CriteriaBuilder builder) { 126 | if (embeddableValue == null) { 127 | return null; 128 | } 129 | 130 | Class type = embeddablePath.getModel().getBindableJavaType(); 131 | ManagedType mt = em.getMetamodel().embeddable(type); // note: calling .managedType() does not work 132 | return jpaUtil.orPredicate(builder, byExample(mt, embeddablePath, embeddableValue, sp, builder)); 133 | } 134 | 135 | /* 136 | * Add a predicate for each simple property whose value is not null. 137 | */ 138 | public List byExample(ManagedType mt, Path mtPath, T mtValue, SearchParameters sp, CriteriaBuilder builder) { 139 | List predicates = newArrayList(); 140 | for (SingularAttribute attr : mt.getSingularAttributes()) { 141 | if (!isPrimaryKey(mt, attr)) { 142 | continue; 143 | } 144 | 145 | Object attrValue = jpaUtil.getValue(mtValue, attr); 146 | if (attrValue != null) { 147 | predicates.add(builder.equal(mtPath.get(jpaUtil.attribute(mt, attr)), attrValue)); 148 | } 149 | } 150 | return predicates; 151 | } 152 | 153 | private boolean isPrimaryKey(ManagedType mt, SingularAttribute attr) { 154 | if (mt.getJavaType().getAnnotation(Embeddable.class) != null) { 155 | return true; 156 | } 157 | return jpaUtil.isPk(mt, attr); 158 | } 159 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/ByNamedQueryUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import javax.inject.Inject; 22 | import javax.inject.Named; 23 | import javax.inject.Singleton; 24 | import javax.persistence.EntityManager; 25 | import javax.persistence.PersistenceContext; 26 | import javax.persistence.Query; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Map.Entry; 30 | 31 | /** 32 | * Helper class to create named query supporting dynamic sort order and pagination. 33 | */ 34 | @Named 35 | @Singleton 36 | public class ByNamedQueryUtil { 37 | private static final Logger log = LoggerFactory.getLogger(ByNamedQueryUtil.class); 38 | 39 | @PersistenceContext 40 | private EntityManager entityManager; 41 | @Inject 42 | private JpaUtil jpaUtil; 43 | 44 | public ByNamedQueryUtil() { 45 | } 46 | 47 | public ByNamedQueryUtil(EntityManager entityManager) { 48 | this.entityManager = entityManager; 49 | } 50 | 51 | protected EntityManager getEntityManager() { 52 | return entityManager; 53 | } 54 | 55 | public List findByNamedQuery(SearchParameters sp) { 56 | if (sp == null || !sp.hasNamedQuery()) { 57 | throw new IllegalArgumentException("searchParameters must be non null and must have a namedQuery"); 58 | } 59 | 60 | Query query = entityManager.createNamedQuery(sp.getNamedQuery()); 61 | String queryString = getQueryString(query); 62 | 63 | // append order by if needed 64 | if (queryString != null && sp.hasOrders()) { 65 | // create the sql restriction clausis 66 | StringBuilder orderClausis = new StringBuilder("order by "); 67 | boolean first = true; 68 | for (OrderBy orderBy : sp.getOrders()) { 69 | if (!first) { 70 | orderClausis.append(", "); 71 | } 72 | orderClausis.append(orderBy.getPath()); 73 | orderClausis.append(orderBy.isOrderDesc() ? " desc" : " asc"); 74 | first = false; 75 | } 76 | 77 | log.debug("appending: [{}] to {}", orderClausis, queryString); 78 | 79 | query = recreateQuery(query, queryString + " " + orderClausis.toString()); 80 | } 81 | 82 | // pagination 83 | jpaUtil.applyPagination(query, sp); 84 | 85 | // named parameters 86 | setQueryParameters(query, sp); 87 | 88 | // execute 89 | @SuppressWarnings("unchecked") 90 | List result = query.getResultList(); 91 | 92 | if (result != null) { 93 | log.debug("{} returned a List of size: {}", sp.getNamedQuery(), result.size()); 94 | } 95 | 96 | return result; 97 | } 98 | 99 | @SuppressWarnings("unchecked") 100 | public T byNamedQuery(SearchParameters sp) { 101 | return (T) objectByNamedQuery(sp); 102 | } 103 | 104 | public Number numberByNamedQuery(SearchParameters sp) { 105 | return (Number) objectByNamedQuery(sp); 106 | } 107 | 108 | public Object objectByNamedQuery(SearchParameters sp) { 109 | if (sp == null || !sp.hasNamedQuery()) { 110 | throw new IllegalStateException("Invalid search template provided: could not determine which namedQuery to use"); 111 | } 112 | 113 | Query query = entityManager.createNamedQuery(sp.getNamedQuery()); 114 | String queryString = getQueryString(query); 115 | 116 | // append select count if needed 117 | if (queryString != null && queryString.toLowerCase().startsWith("from") && !queryString.toLowerCase().contains("count(")) { 118 | query = recreateQuery(query, "select count(*) " + queryString); 119 | } 120 | 121 | setQueryParameters(query, sp); 122 | 123 | log.debug("objectNamedQuery : {}", sp.toString()); 124 | 125 | // execute 126 | Object result = query.getSingleResult(); 127 | 128 | if (log.isDebugEnabled()) { 129 | log.debug("{} returned a {} object", sp.getNamedQuery(), result == null ? "null" : result.getClass()); 130 | if (result instanceof Number) { 131 | log.debug("{} returned a number with value : {}", sp.getNamedQuery(), result); 132 | } 133 | } 134 | 135 | return result; 136 | } 137 | 138 | private void setQueryParameters(Query query, SearchParameters sp) { 139 | // add parameters for the named query 140 | for (Entry entrySet : sp.getNamedQueryParameters().entrySet()) { 141 | query.setParameter(entrySet.getKey(), entrySet.getValue()); 142 | } 143 | } 144 | 145 | /** 146 | * If the named query has the "query" hint, it uses the hint value (which must be jpa QL) to create a new query and append to it the proper order by clause. 147 | */ 148 | private String getQueryString(Query query) { 149 | Map hints = query.getHints(); 150 | return hints != null ? (String) hints.get("query") : null; 151 | } 152 | 153 | private Query recreateQuery(Query current, String newSqlString) { 154 | Query result = entityManager.createQuery(newSqlString); 155 | for (Entry hint : current.getHints().entrySet()) { 156 | result.setHint(hint.getKey(), hint.getValue()); 157 | } 158 | return result; 159 | } 160 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/ByPatternUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import javax.inject.Inject; 19 | import javax.inject.Named; 20 | import javax.inject.Singleton; 21 | import javax.persistence.EntityManager; 22 | import javax.persistence.PersistenceContext; 23 | import javax.persistence.criteria.CriteriaBuilder; 24 | import javax.persistence.criteria.Expression; 25 | import javax.persistence.criteria.Predicate; 26 | import javax.persistence.criteria.Root; 27 | import javax.persistence.metamodel.EntityType; 28 | import javax.persistence.metamodel.SingularAttribute; 29 | import java.util.List; 30 | 31 | import static com.google.common.collect.Lists.newArrayList; 32 | import static javax.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_ONE; 33 | import static javax.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_ONE; 34 | 35 | @Named 36 | @Singleton 37 | public class ByPatternUtil { 38 | @PersistenceContext 39 | private EntityManager em; 40 | @Inject 41 | private JpaUtil jpaUtil; 42 | 43 | /* 44 | * Lookup entities having at least one String attribute matching the passed sp's pattern 45 | */ 46 | @SuppressWarnings("unchecked") 47 | public Predicate byPattern(Root root, CriteriaBuilder builder, SearchParameters sp, Class type) { 48 | if (!sp.hasSearchPattern()) { 49 | return null; 50 | } 51 | 52 | List predicates = newArrayList(); 53 | EntityType entity = em.getMetamodel().entity(type); 54 | String pattern = sp.getSearchPattern(); 55 | 56 | for (SingularAttribute attr : entity.getSingularAttributes()) { 57 | if (attr.getPersistentAttributeType() == MANY_TO_ONE || attr.getPersistentAttributeType() == ONE_TO_ONE) { 58 | continue; 59 | } 60 | 61 | if (attr.getJavaType() == String.class) { 62 | predicates.add(jpaUtil.stringPredicate((Expression) root.get(jpaUtil.attribute(entity, attr)), pattern, sp, builder)); 63 | } 64 | } 65 | 66 | return jpaUtil.orPredicate(builder, predicates); 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/ByPropertySelectorUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import javax.inject.Inject; 19 | import javax.inject.Named; 20 | import javax.inject.Singleton; 21 | import javax.persistence.criteria.CriteriaBuilder; 22 | import javax.persistence.criteria.Path; 23 | import javax.persistence.criteria.Predicate; 24 | import javax.persistence.criteria.Root; 25 | import java.io.Serializable; 26 | import java.util.List; 27 | 28 | import static com.google.common.collect.Lists.newArrayList; 29 | import static org.apache.commons.collections.CollectionUtils.isNotEmpty; 30 | 31 | /** 32 | * Helper to create a predicate out of {@link PropertySelector}s. 33 | */ 34 | @Named 35 | @Singleton 36 | public class ByPropertySelectorUtil { 37 | 38 | @Inject 39 | private JpaUtil jpaUtil; 40 | 41 | @SuppressWarnings("unchecked") 42 | public Predicate byPropertySelectors(Root root, CriteriaBuilder builder, SearchParameters sp) { 43 | List predicates = newArrayList(); 44 | 45 | for (PropertySelector selector : sp.getProperties()) { 46 | if (selector.isBoolean()) { 47 | byBooleanSelector(root, builder, predicates, sp, (PropertySelector) selector); 48 | } else if (selector.isString()) { 49 | byStringSelector(root, builder, predicates, sp, (PropertySelector) selector); 50 | } else { 51 | byObjectSelector(root, builder, predicates, sp, (PropertySelector) selector); 52 | } 53 | } 54 | return jpaUtil.concatPredicate(sp, builder, predicates); 55 | } 56 | 57 | private void byBooleanSelector(Root root, CriteriaBuilder builder, List predicates, SearchParameters sp, 58 | PropertySelector selector) { 59 | if (selector.isNotEmpty()) { 60 | List selectorPredicates = newArrayList(); 61 | 62 | for (Boolean selection : selector.getSelected()) { 63 | Path path = jpaUtil.getPath(root, selector.getAttributes()); 64 | if (selection == null) { 65 | selectorPredicates.add(builder.isNull(path)); 66 | } else { 67 | selectorPredicates.add(selection ? builder.isTrue(path) : builder.isFalse(path)); 68 | } 69 | } 70 | if (selector.isOrMode()) { 71 | predicates.add(jpaUtil.orPredicate(builder, selectorPredicates)); 72 | } else { 73 | predicates.add(jpaUtil.andPredicate(builder, selectorPredicates)); 74 | } 75 | } 76 | } 77 | 78 | private void byStringSelector(Root root, CriteriaBuilder builder, List predicates, SearchParameters sp, 79 | PropertySelector selector) { 80 | if (selector.isNotEmpty()) { 81 | List selectorPredicates = newArrayList(); 82 | 83 | for (String selection : selector.getSelected()) { 84 | Path path = jpaUtil.getPath(root, selector.getAttributes()); 85 | selectorPredicates.add(jpaUtil.stringPredicate(path, selection, selector.getSearchMode(), sp, builder)); 86 | } 87 | if (selector.isOrMode()) { 88 | predicates.add(jpaUtil.orPredicate(builder, selectorPredicates)); 89 | } else { 90 | predicates.add(jpaUtil.andPredicate(builder, selectorPredicates)); 91 | } 92 | } 93 | } 94 | 95 | private void byObjectSelector(Root root, CriteriaBuilder builder, List predicates, SearchParameters sp, 96 | PropertySelector selector) { 97 | if (selector.isNotEmpty()) { 98 | if (selector.isOrMode()) { 99 | byObjectOrModeSelector(root, builder, predicates, sp, selector); 100 | } else { 101 | byObjectAndModeSelector(root, builder, predicates, sp, selector); 102 | } 103 | } else if (selector.isNotIncludingNullSet()) { 104 | predicates.add(builder.isNotNull(jpaUtil.getPath(root, selector.getAttributes()))); 105 | } 106 | } 107 | 108 | private void byObjectOrModeSelector(Root root, CriteriaBuilder builder, List predicates, SearchParameters sp, 109 | PropertySelector selector) { 110 | List selectorPredicates = newArrayList(); 111 | Path path = jpaUtil.getPath(root, selector.getAttributes()); 112 | List selected = selector.getSelected(); 113 | if (selected.contains(null)) { 114 | selected = newArrayList(selector.getSelected()); 115 | selected.remove(null); 116 | selectorPredicates.add(builder.isNull(path)); 117 | } 118 | if (isNotEmpty(selected)) { 119 | if (selected.get(0) instanceof Identifiable) { 120 | List ids = newArrayList(); 121 | for (Object selection : selected) { 122 | ids.add(((Identifiable) selection).getId()); 123 | } 124 | selectorPredicates.add(path.get("id").in(ids)); 125 | } else { 126 | selectorPredicates.add(path.in(selected)); 127 | } 128 | } 129 | predicates.add(jpaUtil.orPredicate(builder, selectorPredicates)); 130 | } 131 | 132 | private void byObjectAndModeSelector(Root root, CriteriaBuilder builder, List predicates, SearchParameters sp, 133 | PropertySelector selector) { 134 | List selectorPredicates = newArrayList(); 135 | List selected = selector.getSelected(); 136 | if (selected.contains(null)) { 137 | selected = newArrayList(selector.getSelected()); 138 | selected.remove(null); 139 | selectorPredicates.add(builder.isNull(jpaUtil.getPath(root, selector.getAttributes()))); 140 | } 141 | for (Object selection : selected) { 142 | Path path = jpaUtil.getPath(root, selector.getAttributes()); 143 | if (selection instanceof Identifiable) { 144 | selectorPredicates.add(builder.equal(path.get("id"), ((Identifiable) selection).getId())); 145 | } else { 146 | selectorPredicates.add(builder.equal(path, selection)); 147 | } 148 | } 149 | predicates.add(jpaUtil.andPredicate(builder, selectorPredicates)); 150 | } 151 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/ByRangeUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import javax.inject.Inject; 19 | import javax.inject.Named; 20 | import javax.inject.Singleton; 21 | import javax.persistence.criteria.CriteriaBuilder; 22 | import javax.persistence.criteria.Path; 23 | import javax.persistence.criteria.Predicate; 24 | import javax.persistence.criteria.Root; 25 | import java.util.List; 26 | 27 | import static com.google.common.collect.Lists.newArrayList; 28 | import static java.lang.Boolean.FALSE; 29 | import static java.lang.Boolean.TRUE; 30 | 31 | /** 32 | * Helper to create a predicate out of {@link Range}s. 33 | */ 34 | @SuppressWarnings("unchecked") 35 | @Named 36 | @Singleton 37 | public class ByRangeUtil { 38 | 39 | @Inject 40 | private JpaUtil jpaUtil; 41 | 42 | public Predicate byRanges(Root root, CriteriaBuilder builder, SearchParameters sp, Class type) { 43 | List> ranges = sp.getRanges(); 44 | List predicates = newArrayList(); 45 | for (Range r : ranges) { 46 | Range range = (Range) r; 47 | if (range.isSet()) { 48 | Predicate rangePredicate = buildRangePredicate(range, root, builder); 49 | if (rangePredicate != null) { 50 | predicates.add(rangePredicate); 51 | } 52 | } 53 | } 54 | 55 | return jpaUtil.concatPredicate(sp, builder, predicates); 56 | } 57 | 58 | private static , E> Predicate buildRangePredicate(Range range, Root root, CriteriaBuilder builder) { 59 | Predicate rangePredicate = null; 60 | Path path = JpaUtil.getInstance().getPath(root, range.getAttributes()); 61 | if (range.isBetween()) { 62 | rangePredicate = builder.between(path, range.getFrom(), range.getTo()); 63 | } else if (range.isFromSet()) { 64 | rangePredicate = builder.greaterThanOrEqualTo(path, range.getFrom()); 65 | } else if (range.isToSet()) { 66 | rangePredicate = builder.lessThanOrEqualTo(path, range.getTo()); 67 | } 68 | 69 | if (rangePredicate != null) { 70 | if (!range.isIncludeNullSet() || range.getIncludeNull() == FALSE) { 71 | return rangePredicate; 72 | } else { 73 | return builder.or(rangePredicate, builder.isNull(path)); 74 | } 75 | } else { 76 | // no from/to is set, but include null or not could be: 77 | if (TRUE == range.getIncludeNull()) { 78 | return builder.isNull(path); 79 | } else if (FALSE == range.getIncludeNull()) { 80 | return builder.isNotNull(path); 81 | } 82 | } 83 | return null; 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/DefaultLuceneQueryBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilter; 19 | import org.apache.lucene.queryparser.classic.QueryParser; 20 | import org.apache.lucene.search.Query; 21 | import org.hibernate.search.jpa.FullTextEntityManager; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import javax.persistence.metamodel.SingularAttribute; 26 | import java.util.List; 27 | 28 | import static com.google.common.base.Preconditions.checkArgument; 29 | import static com.google.common.base.Throwables.propagate; 30 | import static com.google.common.collect.Lists.newArrayList; 31 | import static org.apache.commons.lang.StringUtils.*; 32 | import static org.apache.lucene.queryparser.classic.QueryParser.escape; 33 | 34 | public class DefaultLuceneQueryBuilder implements LuceneQueryBuilder { 35 | 36 | private static final long serialVersionUID = 1L; 37 | private static final Logger log = LoggerFactory.getLogger(DefaultLuceneQueryBuilder.class); 38 | 39 | private static final String SPACES_OR_PUNCTUATION = "\\p{Punct}|\\p{Blank}"; 40 | 41 | @Override 42 | public Query build(FullTextEntityManager fullTextEntityManager, SearchParameters searchParameters, List> availableProperties) { 43 | List clauses = getAllClauses(searchParameters, searchParameters.getTerms(), availableProperties); 44 | 45 | StringBuilder query = new StringBuilder(); 46 | query.append("+("); 47 | for (String clause : clauses) { 48 | if (query.length() > 2) { 49 | query.append(" AND "); 50 | } 51 | query.append(clause); 52 | } 53 | query.append(")"); 54 | 55 | if (query.length() == 3) { 56 | return null; 57 | } 58 | log.debug("Lucene query: {}", query); 59 | try { 60 | return new QueryParser(availableProperties.get(0).getName(), fullTextEntityManager.getSearchFactory().getAnalyzer("custom")) 61 | .parse(query.toString()); 62 | } catch (Exception e) { 63 | throw propagate(e); 64 | } 65 | } 66 | 67 | private List getAllClauses(SearchParameters sp, List terms, List> availableProperties) { 68 | List clauses = newArrayList(); 69 | for (TermSelector term : terms) { 70 | if (term.isNotEmpty()) { 71 | String clause = getClause(sp, term.getSelected(), term.getAttribute(), term.isOrMode(), availableProperties); 72 | if (isNotBlank(clause)) { 73 | clauses.add(clause); 74 | } 75 | } 76 | } 77 | return clauses; 78 | } 79 | 80 | private String getClause(SearchParameters sp, List terms, SingularAttribute property, boolean orMode, 81 | List> availableProperties) { 82 | if (property != null) { 83 | checkArgument(availableProperties.contains(property), property + " is not indexed"); 84 | StringBuilder subQuery = new StringBuilder(); 85 | if (terms != null) { 86 | subQuery.append("("); 87 | for (String wordWithSpacesOrPunctuation : terms) { 88 | if (isBlank(wordWithSpacesOrPunctuation)) { 89 | continue; 90 | } 91 | List wordElements = newArrayList(); 92 | for (String str : wordWithSpacesOrPunctuation.split(SPACES_OR_PUNCTUATION)) { 93 | if (isNotBlank(str)) { 94 | wordElements.add(str); 95 | } 96 | } 97 | if (!wordElements.isEmpty()) { 98 | if (subQuery.length() > 1) { 99 | subQuery.append(" ").append(orMode ? "OR" : "AND").append(" "); 100 | } 101 | subQuery.append(buildSubQuery(property, wordElements, sp)); 102 | } 103 | } 104 | subQuery.append(")"); 105 | } 106 | if (subQuery.length() > 2) { 107 | return subQuery.toString(); 108 | } 109 | } else { 110 | return getOnAnyClause(sp, terms, availableProperties, orMode, availableProperties); 111 | } 112 | return null; 113 | } 114 | 115 | private String buildSubQuery(SingularAttribute property, List terms, SearchParameters sp) { 116 | StringBuilder subQuery = new StringBuilder(); 117 | subQuery.append("("); 118 | for (String term : terms) { 119 | if (subQuery.length() > 1) { 120 | subQuery.append(" AND "); 121 | } 122 | if (sp.getSearchSimilarity() != null) { 123 | subQuery.append(property.getName() + ":" + escapeForFuzzy(lowerCase(term)) + "~" + sp.getSearchSimilarity()); 124 | } else { 125 | subQuery.append(property.getName() + ":" + escape(lowerCase(term))); 126 | } 127 | } 128 | subQuery.append(")"); 129 | return subQuery.toString(); 130 | } 131 | 132 | private String getOnAnyClause(SearchParameters sp, List terms, List> properties, boolean orMode, 133 | List> availableProperties) { 134 | List subClauses = newArrayList(); 135 | for (SingularAttribute property : properties) { 136 | String clause = getClause(sp, terms, property, orMode, availableProperties); 137 | if (isNotBlank(clause)) { 138 | subClauses.add(clause); 139 | } 140 | } 141 | if (subClauses.isEmpty()) { 142 | return null; 143 | } 144 | if (subClauses.size() > 1) { 145 | StringBuilder subQuery = new StringBuilder(); 146 | subQuery.append("("); 147 | for (String subClause : subClauses) { 148 | if (subQuery.length() > 1) { 149 | subQuery.append(" OR "); 150 | } 151 | subQuery.append(subClause); 152 | } 153 | subQuery.append(")"); 154 | return subQuery.toString(); 155 | } else { 156 | return subClauses.get(0); 157 | } 158 | } 159 | 160 | /** 161 | * Apply same filtering as "custom" analyzer. Lowercase is done by QueryParser for fuzzy search. 162 | * 163 | * @param word word 164 | * @return word escaped 165 | */ 166 | private String escapeForFuzzy(String word) { 167 | int length = word.length(); 168 | char[] tmp = new char[length * 4]; 169 | length = ASCIIFoldingFilter.foldToASCII(word.toCharArray(), 0, tmp, 0, length); 170 | return new String(tmp, 0, length); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/EntityGraphLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.springframework.transaction.annotation.Transactional; 19 | 20 | import java.io.Serializable; 21 | import java.util.Collection; 22 | 23 | /** 24 | * The EntityGraphLoader is used to load within a single read-only transaction all the desired associations that 25 | * are normally lazily loaded. 26 | */ 27 | public abstract class EntityGraphLoader, PK extends Serializable> { 28 | 29 | protected GenericRepository repository; 30 | 31 | // required by cglib to create a proxy around the object as we are using the @Transactional annotation 32 | public EntityGraphLoader() { 33 | } 34 | 35 | public EntityGraphLoader(GenericRepository repository) { 36 | this.repository = repository; 37 | } 38 | 39 | /* 40 | * Get the entity by id and load its graph using loadGraph. 41 | */ 42 | @Transactional(readOnly = true) 43 | public T getById(PK pk) { 44 | T entity = repository.getById(pk); 45 | loadGraph(entity); 46 | return entity; 47 | } 48 | 49 | /* 50 | * Merge the passed entity and load the graph of the merged entity using loadGraph. 51 | */ 52 | @Transactional(readOnly = true) 53 | public T merge(T entity) { 54 | T mergedEntity = repository.merge(entity); 55 | loadGraph(mergedEntity); 56 | return mergedEntity; 57 | } 58 | 59 | /* 60 | * Load whatever is needed in the graph of the passed entity, for example x-to-many collection, x-to-one object, etc. 61 | */ 62 | public abstract void loadGraph(T entity); 63 | 64 | /* 65 | * Load the passed 'x-to-many' association. 66 | */ 67 | protected void loadCollection(Collection collection) { 68 | if (collection != null) { 69 | collection.size(); 70 | } 71 | } 72 | 73 | /* 74 | * Load the passed 'x-to-one' association. 75 | */ 76 | protected void loadSingular(Object association) { 77 | if (association != null) { 78 | association.toString(); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/GenericRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.hibernate.search.annotations.Field; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.transaction.annotation.Transactional; 22 | 23 | import javax.annotation.PostConstruct; 24 | import javax.inject.Inject; 25 | import javax.persistence.*; 26 | import javax.persistence.criteria.*; 27 | import javax.persistence.metamodel.Attribute; 28 | import javax.persistence.metamodel.EntityType; 29 | import javax.persistence.metamodel.PluralAttribute; 30 | import javax.persistence.metamodel.SingularAttribute; 31 | import java.io.Serializable; 32 | import java.lang.reflect.Method; 33 | import java.util.List; 34 | 35 | import static com.google.common.base.Preconditions.checkNotNull; 36 | import static com.google.common.collect.Lists.newArrayList; 37 | 38 | /** 39 | * JPA 2 {@link GenericRepository} implementation 40 | */ 41 | public abstract class GenericRepository, PK extends Serializable> { 42 | @Inject 43 | protected ByExampleUtil byExampleUtil; 44 | @Inject 45 | protected ByPatternUtil byPatternUtil; 46 | @Inject 47 | protected ByRangeUtil byRangeUtil; 48 | @Inject 49 | protected ByNamedQueryUtil byNamedQueryUtil; 50 | @Inject 51 | protected ByPropertySelectorUtil byPropertySelectorUtil; 52 | @Inject 53 | protected OrderByUtil orderByUtil; 54 | @Inject 55 | protected MetamodelUtil metamodelUtil; 56 | @Inject 57 | private JpaUtil jpaUtil; 58 | @Inject 59 | protected ByFullTextUtil byFullTextUtil; 60 | protected List> indexedAttributes; 61 | @PersistenceContext 62 | protected EntityManager entityManager; 63 | protected Class type; 64 | protected Logger log; 65 | protected String cacheRegion; 66 | 67 | /* 68 | * This constructor needs the real type of the generic type E so it can be given to the {@link javax.persistence.EntityManager}. 69 | */ 70 | public GenericRepository(Class type) { 71 | this.type = type; 72 | this.log = LoggerFactory.getLogger(getClass()); 73 | this.cacheRegion = type.getCanonicalName(); 74 | } 75 | 76 | @PostConstruct 77 | protected void init() { 78 | this.indexedAttributes = buildIndexedAttributes(type); 79 | } 80 | 81 | public Class getType() { 82 | return type; 83 | } 84 | 85 | /** 86 | * Create a new instance of the repository type. 87 | * 88 | * @return a new instance with no property set. 89 | */ 90 | public abstract E getNew(); 91 | 92 | /** 93 | * Creates a new instance and initializes it with some default values. 94 | * 95 | * @return a new instance initialized with default values. 96 | */ 97 | public E getNewWithDefaults() { 98 | return getNew(); 99 | } 100 | 101 | /** 102 | * Gets from the repository the E entity instance. 103 | *

104 | * DAO for the local database will typically use the primary key or unique fields of the given entity, while DAO for external repository may use a unique 105 | * field present in the entity as they probably have no knowledge of the primary key. Hence, passing the entity as an argument instead of the primary key 106 | * allows you to switch the DAO more easily. 107 | * 108 | * @param entity an E instance having a primary key set 109 | * @return the corresponding E persistent instance or null if none could be found. 110 | */ 111 | @Transactional(readOnly = true) 112 | public E get(E entity) { 113 | return entity == null ? null : getById(entity.getId()); 114 | } 115 | 116 | @Transactional(readOnly = true) 117 | public E getById(PK pk) { 118 | if (pk == null) { 119 | return null; 120 | } 121 | 122 | E entityFound = entityManager.find(type, pk); 123 | if (entityFound == null) { 124 | log.warn("get returned null with id={}", pk); 125 | } 126 | return entityFound; 127 | } 128 | 129 | /** 130 | * Refresh the given entity with up to date data. Does nothing if the given entity is a new entity (not yet managed). 131 | * 132 | * @param entity the entity to refresh. 133 | */ 134 | @Transactional(readOnly = true) 135 | public void refresh(E entity) { 136 | if (entityManager.contains(entity)) { 137 | entityManager.refresh(entity); 138 | } 139 | } 140 | 141 | /* 142 | * Find and load all instances. 143 | */ 144 | @Transactional(readOnly = true) 145 | public List find() { 146 | return find(getNew(), new SearchParameters()); 147 | } 148 | 149 | /** 150 | * Find and load a list of E instance. 151 | * 152 | * @param entity a sample entity whose non-null properties may be used as search hints 153 | * @return the entities matching the search. 154 | */ 155 | @Transactional(readOnly = true) 156 | public List find(E entity) { 157 | return find(entity, new SearchParameters()); 158 | } 159 | 160 | /** 161 | * Find and load a list of E instance. 162 | * 163 | * @param searchParameters carries additional search information 164 | * @return the entities matching the search. 165 | */ 166 | @Transactional(readOnly = true) 167 | public List find(SearchParameters searchParameters) { 168 | return find(getNew(), searchParameters); 169 | } 170 | 171 | /** 172 | * Find and load a list of E instance. 173 | * 174 | * @param entity a sample entity whose non-null properties may be used as search hints 175 | * @param sp carries additional search information 176 | * @return the entities matching the search. 177 | */ 178 | @Transactional(readOnly = true) 179 | public List find(E entity, SearchParameters sp) { 180 | if (sp.hasNamedQuery()) { 181 | return byNamedQueryUtil.findByNamedQuery(sp); 182 | } 183 | CriteriaBuilder builder = entityManager.getCriteriaBuilder(); 184 | CriteriaQuery criteriaQuery = builder.createQuery(type); 185 | if (sp.getDistinct()) { 186 | criteriaQuery.distinct(true); 187 | } 188 | Root root = criteriaQuery.from(type); 189 | 190 | // predicate 191 | Predicate predicate = getPredicate(criteriaQuery, root, builder, entity, sp); 192 | if (predicate != null) { 193 | criteriaQuery = criteriaQuery.where(predicate); 194 | } 195 | 196 | // fetches 197 | fetches(sp, root); 198 | 199 | // order by 200 | criteriaQuery.orderBy(orderByUtil.buildJpaOrders(sp.getOrders(), root, builder, sp)); 201 | 202 | TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); 203 | applyCacheHints(typedQuery, sp); 204 | jpaUtil.applyPagination(typedQuery, sp); 205 | List entities = typedQuery.getResultList(); 206 | log.debug("Returned {} elements", entities.size()); 207 | 208 | return entities; 209 | } 210 | 211 | /* 212 | * Find a list of E property. 213 | * 214 | * @param propertyType type of the property 215 | * @param entity a sample entity whose non-null properties may be used as search hints 216 | * @param sp carries additional search information 217 | * @param path the path to the property 218 | * @return the entities property matching the search. 219 | */ 220 | @Transactional(readOnly = true) 221 | public List findProperty(Class propertyType, E entity, SearchParameters sp, String path) { 222 | return findProperty(propertyType, entity, sp, metamodelUtil.toAttributes(path, type)); 223 | } 224 | 225 | /* 226 | * Find a list of E property. 227 | * 228 | * @param propertyType type of the property 229 | * @param entity a sample entity whose non-null properties may be used as search hints 230 | * @param sp carries additional search information 231 | * @param attributes the list of attributes to the property 232 | * @return the entities property matching the search. 233 | */ 234 | @Transactional(readOnly = true) 235 | public List findProperty(Class propertyType, E entity, SearchParameters sp, Attribute... attributes) { 236 | return findProperty(propertyType, entity, sp, newArrayList(attributes)); 237 | } 238 | 239 | /* 240 | * Find a list of E property. 241 | * 242 | * @param propertyType type of the property 243 | * @param entity a sample entity whose non-null properties may be used as search hints 244 | * @param sp carries additional search information 245 | * @param attributes the list of attributes to the property 246 | * @return the entities property matching the search. 247 | */ 248 | @Transactional(readOnly = true) 249 | public List findProperty(Class propertyType, E entity, SearchParameters sp, List> attributes) { 250 | if (sp.hasNamedQuery()) { 251 | return byNamedQueryUtil.findByNamedQuery(sp); 252 | } 253 | CriteriaBuilder builder = entityManager.getCriteriaBuilder(); 254 | CriteriaQuery criteriaQuery = builder.createQuery(propertyType); 255 | if (sp.getDistinct()) { 256 | criteriaQuery.distinct(true); 257 | } 258 | Root root = criteriaQuery.from(type); 259 | Path path = jpaUtil.getPath(root, attributes); 260 | criteriaQuery.select(path); 261 | 262 | // predicate 263 | Predicate predicate = getPredicate(criteriaQuery, root, builder, entity, sp); 264 | if (predicate != null) { 265 | criteriaQuery = criteriaQuery.where(predicate); 266 | } 267 | 268 | // fetches 269 | fetches(sp, root); 270 | 271 | // order by 272 | // we do not want to follow order by specified in search parameters 273 | criteriaQuery.orderBy(builder.asc(path)); 274 | 275 | TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); 276 | applyCacheHints(typedQuery, sp); 277 | jpaUtil.applyPagination(typedQuery, sp); 278 | List entities = typedQuery.getResultList(); 279 | log.debug("Returned {} elements", entities.size()); 280 | 281 | return entities; 282 | } 283 | 284 | /** 285 | * Count the number of E instances. 286 | * 287 | * @param sp carries additional search information 288 | * @return the number of entities matching the search. 289 | */ 290 | @Transactional(readOnly = true) 291 | public int findCount(SearchParameters sp) { 292 | return findCount(getNew(), sp); 293 | } 294 | 295 | /** 296 | * Count the number of E instances. 297 | * 298 | * @param entity a sample entity whose non-null properties may be used as search hint 299 | * @return the number of entities matching the search. 300 | */ 301 | @Transactional(readOnly = true) 302 | public int findCount(E entity) { 303 | return findCount(entity, new SearchParameters()); 304 | } 305 | 306 | /** 307 | * Count the number of E instances. 308 | * 309 | * @param entity a sample entity whose non-null properties may be used as search hint 310 | * @param sp carries additional search information 311 | * @return the number of entities matching the search. 312 | */ 313 | @Transactional(readOnly = true) 314 | public int findCount(E entity, SearchParameters sp) { 315 | checkNotNull(entity, "The entity cannot be null"); 316 | checkNotNull(sp, "The searchParameters cannot be null"); 317 | 318 | if (sp.hasNamedQuery()) { 319 | return byNamedQueryUtil.numberByNamedQuery(sp).intValue(); 320 | } 321 | CriteriaBuilder builder = entityManager.getCriteriaBuilder(); 322 | 323 | CriteriaQuery criteriaQuery = builder.createQuery(Long.class); 324 | Root root = criteriaQuery.from(type); 325 | 326 | if (sp.getDistinct()) { 327 | criteriaQuery = criteriaQuery.select(builder.countDistinct(root)); 328 | } else { 329 | criteriaQuery = criteriaQuery.select(builder.count(root)); 330 | } 331 | 332 | // predicate 333 | Predicate predicate = getPredicate(criteriaQuery, root, builder, entity, sp); 334 | if (predicate != null) { 335 | criteriaQuery = criteriaQuery.where(predicate); 336 | } 337 | 338 | // construct order by to fetch or joins if needed 339 | orderByUtil.buildJpaOrders(sp.getOrders(), root, builder, sp); 340 | 341 | TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); 342 | 343 | applyCacheHints(typedQuery, sp); 344 | return typedQuery.getSingleResult().intValue(); 345 | } 346 | 347 | /** 348 | * Count the number of E instances. 349 | * 350 | * @param entity a sample entity whose non-null properties may be used as search hint 351 | * @param sp carries additional search information 352 | * @param path the path to the property 353 | * @return the number of entities matching the search. 354 | */ 355 | @Transactional(readOnly = true) 356 | public int findPropertyCount(E entity, SearchParameters sp, String path) { 357 | return findPropertyCount(entity, sp, metamodelUtil.toAttributes(path, type)); 358 | } 359 | 360 | /** 361 | * Count the number of E instances. 362 | * 363 | * @param entity a sample entity whose non-null properties may be used as search hint 364 | * @param sp carries additional search information 365 | * @param attributes the list of attributes to the property 366 | * @return the number of entities matching the search. 367 | */ 368 | @Transactional(readOnly = true) 369 | public int findPropertyCount(E entity, SearchParameters sp, Attribute... attributes) { 370 | return findPropertyCount(entity, sp, newArrayList(attributes)); 371 | } 372 | 373 | /** 374 | * Count the number of E instances. 375 | * 376 | * @param entity a sample entity whose non-null properties may be used as search hint 377 | * @param sp carries additional search information 378 | * @param attributes the list of attributes to the property 379 | * @return the number of entities matching the search. 380 | */ 381 | @Transactional(readOnly = true) 382 | public int findPropertyCount(E entity, SearchParameters sp, List> attributes) { 383 | if (sp.hasNamedQuery()) { 384 | return byNamedQueryUtil.numberByNamedQuery(sp).intValue(); 385 | } 386 | CriteriaBuilder builder = entityManager.getCriteriaBuilder(); 387 | CriteriaQuery criteriaQuery = builder.createQuery(Long.class); 388 | Root root = criteriaQuery.from(type); 389 | Path path = jpaUtil.getPath(root, attributes); 390 | 391 | if (sp.getDistinct()) { 392 | criteriaQuery = criteriaQuery.select(builder.countDistinct(path)); 393 | } else { 394 | criteriaQuery = criteriaQuery.select(builder.count(path)); 395 | } 396 | 397 | // predicate 398 | Predicate predicate = getPredicate(criteriaQuery, root, builder, entity, sp); 399 | if (predicate != null) { 400 | criteriaQuery = criteriaQuery.where(predicate); 401 | } 402 | 403 | // construct order by to fetch or joins if needed 404 | orderByUtil.buildJpaOrders(sp.getOrders(), root, builder, sp); 405 | 406 | TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); 407 | 408 | applyCacheHints(typedQuery, sp); 409 | return typedQuery.getSingleResult().intValue(); 410 | } 411 | 412 | @Transactional(readOnly = true) 413 | public E findUnique(SearchParameters sp) { 414 | return findUnique(getNew(), sp); 415 | } 416 | 417 | @Transactional(readOnly = true) 418 | public E findUnique(E e) { 419 | return findUnique(e, new SearchParameters()); 420 | } 421 | 422 | @Transactional(readOnly = true) 423 | public E findUnique(E entity, SearchParameters sp) { 424 | E result = findUniqueOrNone(entity, sp); 425 | if (result != null) { 426 | return result; 427 | } 428 | throw new NoResultException("Developper: You expected 1 result but found none !"); 429 | } 430 | 431 | @Transactional(readOnly = true) 432 | public E findUniqueOrNone(SearchParameters sp) { 433 | return findUniqueOrNone(getNew(), sp); 434 | } 435 | 436 | @Transactional(readOnly = true) 437 | public E findUniqueOrNone(E entity) { 438 | return findUniqueOrNone(entity, new SearchParameters()); 439 | } 440 | 441 | /* 442 | * We request at most 2, if there's more than one then we throw a {@link javax.persistence.NonUniqueResultException} 443 | * 444 | * @throws javax.persistence.NonUniqueResultException 445 | */ 446 | @Transactional(readOnly = true) 447 | public E findUniqueOrNone(E entity, SearchParameters sp) { 448 | // this code is an optimization to prevent using a count 449 | sp.setFirst(0); 450 | sp.setMaxResults(2); 451 | List results = find(entity, sp); 452 | 453 | if (results == null || results.isEmpty()) { 454 | return null; 455 | } else if (results.size() > 1) { 456 | throw new NonUniqueResultException("Developper: You expected 1 result but we found more ! sample: " + entity); 457 | } else { 458 | return results.iterator().next(); 459 | } 460 | } 461 | 462 | @Transactional(readOnly = true) 463 | public E findUniqueOrNew(SearchParameters sp) { 464 | return findUniqueOrNew(getNew(), sp); 465 | } 466 | 467 | @Transactional(readOnly = true) 468 | public E findUniqueOrNew(E e) { 469 | return findUniqueOrNew(e, new SearchParameters()); 470 | } 471 | 472 | @Transactional(readOnly = true) 473 | public E findUniqueOrNew(E entity, SearchParameters sp) { 474 | E result = findUniqueOrNone(entity, sp); 475 | if (result != null) { 476 | return result; 477 | } else { 478 | return getNewWithDefaults(); 479 | } 480 | } 481 | 482 | protected Predicate getPredicate(CriteriaQuery criteriaQuery, Root root, CriteriaBuilder builder, E entity, SearchParameters sp) { 483 | return jpaUtil.andPredicate(builder, // 484 | bySearchPredicate(root, builder, entity, sp), // 485 | byMandatoryPredicate(criteriaQuery, root, builder, entity, sp)); 486 | } 487 | 488 | protected Predicate bySearchPredicate(Root root, CriteriaBuilder builder, E entity, SearchParameters sp) { 489 | return jpaUtil.concatPredicate(sp, builder, // 490 | byFullText(root, builder, sp, entity, indexedAttributes), // 491 | byRanges(root, builder, sp, type), // 492 | byPropertySelectors(root, builder, sp), // 493 | byExample(root, builder, sp, entity), // 494 | byPattern(root, builder, sp, type)); 495 | } 496 | 497 | protected > Predicate byFullText(Root root, CriteriaBuilder builder, SearchParameters sp, T entity, 498 | List> indexedAttributes) { 499 | return byFullTextUtil.byFullText(root, builder, sp, entity, indexedAttributes); 500 | } 501 | 502 | protected Predicate byExample(Root root, CriteriaBuilder builder, SearchParameters sp, E entity) { 503 | return byExampleUtil.byExampleOnEntity(root, entity, builder, sp); 504 | } 505 | 506 | protected Predicate byPropertySelectors(Root root, CriteriaBuilder builder, SearchParameters sp) { 507 | return byPropertySelectorUtil.byPropertySelectors(root, builder, sp); 508 | } 509 | 510 | protected Predicate byRanges(Root root, CriteriaBuilder builder, SearchParameters sp, Class type) { 511 | return byRangeUtil.byRanges(root, builder, sp, type); 512 | } 513 | 514 | protected Predicate byPattern(Root root, CriteriaBuilder builder, SearchParameters sp, Class type) { 515 | return byPatternUtil.byPattern(root, builder, sp, type); 516 | } 517 | 518 | /* 519 | * You may override this method to add a Predicate to the default find method. 520 | */ 521 | protected Predicate byMandatoryPredicate(CriteriaQuery criteriaQuery, Root root, CriteriaBuilder builder, E entity, SearchParameters sp) { 522 | return null; 523 | } 524 | 525 | /** 526 | * Save or update the given entity E to the repository. Assume that the entity is already present in the persistence context. No merge is done. 527 | * 528 | * @param entity the entity to be saved or updated. 529 | */ 530 | @Transactional 531 | public void save(E entity) { 532 | checkNotNull(entity, "The entity to save cannot be null"); 533 | 534 | // creation with auto generated id 535 | if (!entity.isIdSet()) { 536 | entityManager.persist(entity); 537 | return; 538 | } 539 | 540 | // creation with manually assigned key 541 | if (jpaUtil.isEntityIdManuallyAssigned(type) && !entityManager.contains(entity)) { 542 | entityManager.persist(entity); 543 | return; 544 | } 545 | // other cases are update 546 | // the simple fact to invoke this method, from a service method annotated with @Transactional, 547 | // does the job (assuming the give entity is present in the persistence context) 548 | } 549 | 550 | /* 551 | * Persist the given entity. 552 | */ 553 | @Transactional 554 | public void persist(E entity) { 555 | entityManager.persist(entity); 556 | } 557 | 558 | /* 559 | * Merge the state of the given entity into the current persistence context. 560 | */ 561 | @Transactional 562 | public E merge(E entity) { 563 | return entityManager.merge(entity); 564 | } 565 | 566 | /* 567 | * Delete the given entity E from the repository. 568 | * 569 | * @param entity the entity to be deleted. 570 | */ 571 | @Transactional 572 | public void delete(E entity) { 573 | if (entityManager.contains(entity)) { 574 | entityManager.remove(entity); 575 | } else { 576 | // could be a delete on a transient instance 577 | E entityRef = entityManager.getReference(type, entity.getId()); 578 | 579 | if (entityRef != null) { 580 | entityManager.remove(entityRef); 581 | } else { 582 | log.warn("Attempt to delete an instance that is not present in the database: {}", entity); 583 | } 584 | } 585 | } 586 | 587 | protected List> buildIndexedAttributes(Class type) { 588 | List> ret = newArrayList(); 589 | for (Method m : type.getMethods()) { 590 | if (m.getAnnotation(Field.class) != null) { 591 | ret.add(metamodelUtil.toAttribute(jpaUtil.methodToProperty(m), type)); 592 | } 593 | } 594 | return ret; 595 | } 596 | 597 | public boolean isIndexed(String property) { 598 | return !property.contains(".") && indexedAttributes.contains(metamodelUtil.toAttribute(property, type)); 599 | } 600 | 601 | // ----------------- 602 | // Util 603 | // ----------------- 604 | 605 | /* 606 | * Helper to determine if the passed given property is null. Used mainly on binary lazy loaded property. 607 | * 608 | * @param id the entity id 609 | * @param property the property to check 610 | */ 611 | @Transactional(readOnly = true) 612 | public boolean isPropertyNull(PK id, SingularAttribute property) { 613 | checkNotNull(id, "The id cannot be null"); 614 | checkNotNull(property, "The property cannot be null"); 615 | CriteriaBuilder builder = entityManager.getCriteriaBuilder(); 616 | CriteriaQuery criteriaQuery = builder.createQuery(Long.class); 617 | Root root = criteriaQuery.from(type); 618 | criteriaQuery = criteriaQuery.select(builder.count(root)); 619 | 620 | // predicate 621 | Predicate idPredicate = builder.equal(root.get("id"), id); 622 | Predicate isNullPredicate = builder.isNull(root.get(property)); 623 | criteriaQuery = criteriaQuery.where(jpaUtil.andPredicate(builder, idPredicate, isNullPredicate)); 624 | 625 | TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); 626 | return typedQuery.getSingleResult().intValue() == 1; 627 | } 628 | 629 | /* 630 | * Return the optimistic version value, if any. 631 | */ 632 | @SuppressWarnings("unchecked") 633 | @Transactional(readOnly = true) 634 | public Comparable getVersion(E entity) { 635 | EntityType entityType = entityManager.getMetamodel().entity(type); 636 | if (!entityType.hasVersionAttribute()) { 637 | return null; 638 | } 639 | return (Comparable) jpaUtil.getValue(entity, getVersionAttribute(entityType)); 640 | } 641 | 642 | /* 643 | * _HACK_ too bad that JPA does not provide this entityType.getVersion(); 644 | *

645 | * http://stackoverflow.com/questions/13265094/generic-way-to-get-jpa-entity-version 646 | */ 647 | protected SingularAttribute getVersionAttribute(EntityType entityType) { 648 | for (SingularAttribute sa : entityType.getSingularAttributes()) { 649 | if (sa.isVersion()) { 650 | return sa; 651 | } 652 | } 653 | return null; 654 | } 655 | 656 | // ----------------- 657 | // Commons 658 | // ----------------- 659 | 660 | /* 661 | * Set hints for 2d level cache. 662 | */ 663 | protected void applyCacheHints(TypedQuery typedQuery, SearchParameters sp) { 664 | if (sp.isCacheable()) { 665 | typedQuery.setHint("org.hibernate.cacheable", true); 666 | 667 | if (sp.hasCacheRegion()) { 668 | typedQuery.setHint("org.hibernate.cacheRegion", sp.getCacheRegion()); 669 | } else { 670 | typedQuery.setHint("org.hibernate.cacheRegion", cacheRegion); 671 | } 672 | } 673 | } 674 | 675 | @SuppressWarnings({"unchecked", "rawtypes"}) 676 | protected void fetches(SearchParameters sp, Root root) { 677 | for (List> args : sp.getFetches()) { 678 | FetchParent from = root; 679 | for (Attribute arg : args) { 680 | boolean found = false; 681 | for (Fetch fetch : from.getFetches()) { 682 | if (arg.equals(fetch.getAttribute())) { 683 | from = fetch; 684 | found = true; 685 | break; 686 | } 687 | } 688 | if (!found) { 689 | if (arg instanceof PluralAttribute) { 690 | from = from.fetch((PluralAttribute) arg, JoinType.LEFT); 691 | } else { 692 | from = from.fetch((SingularAttribute) arg, JoinType.LEFT); 693 | } 694 | } 695 | } 696 | } 697 | } 698 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/HibernateSearchUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.apache.lucene.search.Query; 19 | import org.hibernate.search.jpa.FullTextEntityManager; 20 | import org.hibernate.search.jpa.FullTextQuery; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import javax.inject.Named; 25 | import javax.inject.Singleton; 26 | import javax.persistence.EntityManager; 27 | import javax.persistence.PersistenceContext; 28 | import javax.persistence.metamodel.SingularAttribute; 29 | import java.io.Serializable; 30 | import java.util.List; 31 | 32 | import static com.google.common.collect.Lists.newArrayList; 33 | import static org.hibernate.search.jpa.Search.getFullTextEntityManager; 34 | 35 | @Named 36 | @Singleton 37 | public class HibernateSearchUtil { 38 | private static final Logger log = LoggerFactory.getLogger(HibernateSearchUtil.class); 39 | 40 | @PersistenceContext 41 | private EntityManager entityManager; 42 | 43 | @SuppressWarnings("unchecked") 44 | public List find(Class clazz, SearchParameters sp, List> availableProperties) { 45 | log.info("Searching {} with terms : {} with available Properties: {}", new Object[]{clazz.getSimpleName(), sp.getTerms(), availableProperties}); 46 | FullTextEntityManager fullTextEntityManager = getFullTextEntityManager(entityManager); 47 | Query query = sp.getLuceneQueryBuilder().build(fullTextEntityManager, sp, availableProperties); 48 | 49 | if (query == null) { 50 | return null; 51 | } 52 | 53 | FullTextQuery ftq = fullTextEntityManager.createFullTextQuery( // 54 | query, clazz); 55 | if (sp.getMaxResults() > 0) { 56 | ftq.setMaxResults(sp.getMaxResults()); 57 | } 58 | return ftq.getResultList(); 59 | } 60 | 61 | /* 62 | * Same as {@link #find(Class, SearchParameters, List)} but will return only the id 63 | */ 64 | @SuppressWarnings("unchecked") 65 | public List findId(Class clazz, SearchParameters sp, List> availableProperties) { 66 | log.info("Searching {} with terms : {} with available Properties: {}", new Object[]{clazz.getSimpleName(), sp.getTerms(), availableProperties}); 67 | FullTextEntityManager fullTextEntityManager = getFullTextEntityManager(entityManager); 68 | Query query = sp.getLuceneQueryBuilder().build(fullTextEntityManager, sp, availableProperties); 69 | 70 | if (query == null) { 71 | return null; 72 | } 73 | 74 | FullTextQuery ftq = fullTextEntityManager.createFullTextQuery( // 75 | query, clazz); 76 | ftq.setProjection("id"); 77 | if (sp.getMaxResults() > 0) { 78 | ftq.setMaxResults(sp.getMaxResults()); 79 | } 80 | List ids = newArrayList(); 81 | List resultList = ftq.getResultList(); 82 | for (Object[] result : resultList) { 83 | ids.add((Serializable) result[0]); 84 | } 85 | return ids; 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/Identifiable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import java.io.Serializable; 19 | 20 | /** 21 | * By making entities implement this interface we can easily retrieve from the 22 | * {@link com.jaxio.jpa.querybyexample.GenericRepository} the identifier property of the entity. 23 | */ 24 | public interface Identifiable { 25 | 26 | /** 27 | * @return the primary key 28 | */ 29 | PK getId(); 30 | 31 | /** 32 | * Sets the primary key 33 | * 34 | * @param id primary key 35 | */ 36 | void setId(PK id); 37 | 38 | /** 39 | * Helper method to know whether the primary key is set or not. 40 | * 41 | * @return true if the primary key is set, false otherwise 42 | */ 43 | boolean isIdSet(); 44 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/JpaUniqueUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.apache.commons.lang.WordUtils; 19 | import org.springframework.context.i18n.LocaleContextHolder; 20 | 21 | import javax.inject.Inject; 22 | import javax.inject.Named; 23 | import javax.inject.Singleton; 24 | import javax.persistence.*; 25 | import java.lang.reflect.Field; 26 | import java.lang.reflect.Method; 27 | import java.util.List; 28 | import java.util.Map; 29 | 30 | import static com.google.common.collect.Iterables.concat; 31 | import static com.google.common.collect.Lists.newArrayList; 32 | import static com.google.common.collect.Maps.newHashMap; 33 | import static java.util.Collections.emptyList; 34 | import static org.apache.commons.lang.StringUtils.equalsIgnoreCase; 35 | import static org.hibernate.proxy.HibernateProxyHelper.getClassWithoutInitializingProxy; 36 | 37 | @Named 38 | @Singleton 39 | public class JpaUniqueUtil { 40 | @PersistenceContext 41 | private EntityManager entityManager; 42 | @Inject 43 | private JpaUtil jpaUtil; 44 | 45 | /* 46 | * Return the error code if the given property is already present in the database, returns null otherwise. 47 | */ 48 | public String validateSimpleUnique(Identifiable entity, String property, Object value) { 49 | Map values = newHashMap(); 50 | values.put(property, value); 51 | return existsInDatabaseOnAllObjects(entity, values) ? simpleUniqueConstraintError(entity, property) : null; 52 | } 53 | 54 | /* 55 | * Return a list of error codes for all composite unique and simple unique constraints violations. 56 | */ 57 | public List validateUniques(Identifiable entity) { 58 | return newArrayList(concat( // 59 | validateCompositeUniqueConstraints(entity), // 60 | validateSimpleUniqueConstraints(entity) // 61 | )); 62 | } 63 | 64 | private List validateSimpleUniqueConstraints(Identifiable entity) { 65 | return newArrayList(concat( // 66 | validateSimpleUniqueConstraintsDefinedOnMethods(entity), // 67 | validateSimpleUniqueConstraintsDefinedOnFields(entity))); 68 | } 69 | 70 | private List validateSimpleUniqueConstraintsDefinedOnFields(Identifiable entity) { 71 | Class entityClass = getClassWithoutInitializingProxy(entity); 72 | List errors = newArrayList(); 73 | for (Field field : entityClass.getFields()) { 74 | Column column = field.getAnnotation(Column.class); 75 | if (column != null && column.unique()) { 76 | Map values = newHashMap(); 77 | values.put(field.getName(), jpaUtil.getValueFromField(field, entity)); 78 | if (existsInDatabaseOnAllObjects(entity, values)) { 79 | errors.add(simpleUniqueConstraintError(entity, field.getName())); 80 | } 81 | } 82 | } 83 | return errors; 84 | } 85 | 86 | private List validateSimpleUniqueConstraintsDefinedOnMethods(Identifiable entity) { 87 | Class entityClass = getClassWithoutInitializingProxy(entity); 88 | List errors = newArrayList(); 89 | for (Method method : entityClass.getMethods()) { 90 | Column column = entityClass.getAnnotation(Column.class); 91 | if (column != null && column.unique()) { 92 | Map values = newHashMap(); 93 | String property = jpaUtil.methodToProperty(method); 94 | values.put(property, invokeMethod(method, entity)); 95 | if (existsInDatabaseOnAllObjects(entity, values)) { 96 | errors.add(simpleUniqueConstraintError(entity, property)); 97 | } 98 | } 99 | } 100 | return errors; 101 | } 102 | 103 | private String simpleUniqueConstraintError(Identifiable entity, String property) { 104 | return WordUtils.uncapitalize(jpaUtil.getEntityName(entity)) + "_" + property + "_already_exists"; 105 | } 106 | 107 | private List validateCompositeUniqueConstraints(Identifiable entity) { 108 | Class entityClass = getClassWithoutInitializingProxy(entity); 109 | Table table = entityClass.getAnnotation(Table.class); 110 | if (table == null) { 111 | return emptyList(); 112 | } 113 | List errors = newArrayList(); 114 | for (UniqueConstraint uniqueConstraint : table.uniqueConstraints()) { 115 | if (!checkCompositeUniqueConstraint(entity, entityClass, uniqueConstraint)) { 116 | errors.add(compositeUniqueConstraintErrorCode(entity, uniqueConstraint)); 117 | } 118 | } 119 | return errors; 120 | } 121 | 122 | private String compositeUniqueConstraintErrorCode(Identifiable entity, UniqueConstraint uniqueConstraint) { 123 | return WordUtils.uncapitalize(jpaUtil.getEntityName(entity)) + "_" 124 | + (uniqueConstraint.name() == null ? "composite_unique_constraint_error" : uniqueConstraint.name().toLowerCase()); 125 | } 126 | 127 | private boolean checkCompositeUniqueConstraint(Identifiable entity, Class entityClass, UniqueConstraint u) { 128 | Map values = newHashMap(); 129 | values.putAll(getPropertyConstraints(entity, entityClass, u, "")); 130 | return !existsInDatabaseOnAllObjects(entity, values); 131 | } 132 | 133 | private Map getPropertyConstraints(Object entity, Class entityClass, UniqueConstraint u, String prefix) { 134 | Map values = newHashMap(); 135 | for (String column : u.columnNames()) { 136 | Method method = columnNameToMethod(entityClass, column); 137 | if (method != null) { 138 | values.put(prefix + jpaUtil.methodToProperty(method), invokeMethod(method, entity)); 139 | } else { 140 | Field field = columnNameToField(entityClass, column); 141 | if (field != null) { 142 | values.put(prefix + field.getName(), jpaUtil.getValueFromField(field, entity)); 143 | } 144 | } 145 | } 146 | return values; 147 | } 148 | 149 | private Method columnNameToMethod(Class clazz, String columnName) { 150 | for (Method method : clazz.getMethods()) { 151 | Column column = method.getAnnotation(Column.class); 152 | if (column != null && equalsIgnoreCase(columnName, column.name())) { 153 | return method; 154 | } 155 | } 156 | return null; 157 | } 158 | 159 | private Field columnNameToField(Class clazz, String columnName) { 160 | for (Field field : clazz.getFields()) { 161 | Column column = field.getAnnotation(Column.class); 162 | if (equalsIgnoreCase(columnName, column.name())) { 163 | return field; 164 | } 165 | } 166 | return null; 167 | } 168 | 169 | private boolean existsInDatabaseOnAllObjects(Identifiable entity, Map values) { 170 | if (entity == null || values == null || values.isEmpty()) { 171 | return false; 172 | } 173 | String entityName = jpaUtil.getEntityName(entity); 174 | String sqlQuery = "select count(c) from " + entityName + " c where"; 175 | boolean first = true; 176 | for (Map.Entry property : values.entrySet()) { 177 | sqlQuery += !first ? " and " : " "; 178 | if (property.getValue() instanceof String) { 179 | sqlQuery += "upper(" + property.getKey() + ")=:" + property.getKey(); 180 | } else { 181 | sqlQuery += property.getKey() + "=:" + property.getKey(); 182 | } 183 | first = false; 184 | } 185 | if (entity.isIdSet()) { 186 | if (!first) { 187 | sqlQuery += " and"; 188 | } 189 | sqlQuery += " id<>:id"; 190 | } 191 | TypedQuery query = entityManager.createQuery(sqlQuery, Long.class); 192 | for (Map.Entry property : values.entrySet()) { 193 | String propertyName = property.getKey(); 194 | Object value = property.getValue(); 195 | if (value instanceof String) { 196 | value = ((String) value).toUpperCase(LocaleContextHolder.getLocale()); 197 | } 198 | query.setParameter(propertyName, value); 199 | } 200 | if (entity.isIdSet()) { 201 | query.setParameter("id", entity.getId()); 202 | } 203 | return query.getSingleResult() > 0; 204 | } 205 | 206 | @SuppressWarnings("unchecked") 207 | private T invokeMethod(Method method, Object target) { 208 | try { 209 | return (T) method.invoke(target); 210 | } catch (Exception e) { 211 | throw new RuntimeException(e); 212 | } 213 | } 214 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/JpaUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.apache.commons.beanutils.MethodUtils; 19 | import org.apache.commons.beanutils.PropertyUtils; 20 | import org.apache.commons.lang.WordUtils; 21 | import org.springframework.context.annotation.Lazy; 22 | import org.springframework.context.i18n.LocaleContextHolder; 23 | 24 | import javax.inject.Named; 25 | import javax.inject.Singleton; 26 | import javax.persistence.*; 27 | import javax.persistence.criteria.*; 28 | import javax.persistence.metamodel.Attribute; 29 | import javax.persistence.metamodel.ManagedType; 30 | import javax.persistence.metamodel.PluralAttribute; 31 | import javax.persistence.metamodel.SingularAttribute; 32 | import java.beans.PropertyDescriptor; 33 | import java.lang.reflect.Field; 34 | import java.lang.reflect.Method; 35 | import java.util.List; 36 | import java.util.Map; 37 | 38 | import static com.google.common.base.Predicates.notNull; 39 | import static com.google.common.base.Throwables.propagate; 40 | import static com.google.common.collect.Iterables.filter; 41 | import static com.google.common.collect.Iterables.toArray; 42 | import static com.google.common.collect.Lists.newArrayList; 43 | import static com.google.common.collect.Maps.newHashMap; 44 | import static java.lang.reflect.Modifier.isPublic; 45 | import static org.apache.commons.lang.StringUtils.isBlank; 46 | import static org.hibernate.proxy.HibernateProxyHelper.getClassWithoutInitializingProxy; 47 | 48 | @Named 49 | @Singleton 50 | @Lazy(false) 51 | public class JpaUtil { 52 | 53 | private Map, String> compositePkCache = newHashMap(); 54 | private static JpaUtil instance; 55 | 56 | public static JpaUtil getInstance() { 57 | return instance; 58 | } 59 | 60 | public JpaUtil() { 61 | instance = this; 62 | } 63 | 64 | public boolean isEntityIdManuallyAssigned(Class type) { 65 | for (Method method : type.getMethods()) { 66 | if (isPrimaryKey(method)) { 67 | return isManuallyAssigned(method); 68 | } 69 | } 70 | return false; // no pk found, should not happen 71 | } 72 | 73 | private boolean isPrimaryKey(Method method) { 74 | return isPublic(method.getModifiers()) && (method.getAnnotation(Id.class) != null || method.getAnnotation(EmbeddedId.class) != null); 75 | } 76 | 77 | private boolean isManuallyAssigned(Method method) { 78 | if (method.getAnnotation(Id.class) != null) { 79 | return method.getAnnotation(GeneratedValue.class) == null; 80 | } 81 | 82 | return method.getAnnotation(EmbeddedId.class) != null; 83 | } 84 | 85 | public Predicate concatPredicate(SearchParameters sp, CriteriaBuilder builder, Predicate... predicatesNullAllowed) { 86 | return concatPredicate(sp, builder, newArrayList(predicatesNullAllowed)); 87 | } 88 | 89 | public Predicate concatPredicate(SearchParameters sp, CriteriaBuilder builder, Iterable predicatesNullAllowed) { 90 | if (sp.isAndMode()) { 91 | return andPredicate(builder, predicatesNullAllowed); 92 | } else { 93 | return orPredicate(builder, predicatesNullAllowed); 94 | } 95 | } 96 | 97 | public Predicate andPredicate(CriteriaBuilder builder, Predicate... predicatesNullAllowed) { 98 | return andPredicate(builder, newArrayList(predicatesNullAllowed)); 99 | } 100 | 101 | public Predicate orPredicate(CriteriaBuilder builder, Predicate... predicatesNullAllowed) { 102 | return orPredicate(builder, newArrayList(predicatesNullAllowed)); 103 | } 104 | 105 | public Predicate andPredicate(CriteriaBuilder builder, Iterable predicatesNullAllowed) { 106 | List predicates = newArrayList(filter(predicatesNullAllowed, notNull())); 107 | if (predicates == null || predicates.isEmpty()) { 108 | return null; 109 | } else if (predicates.size() == 1) { 110 | return predicates.get(0); 111 | } else { 112 | return builder.and(toArray(predicates, Predicate.class)); 113 | } 114 | } 115 | 116 | public Predicate orPredicate(CriteriaBuilder builder, Iterable predicatesNullAllowed) { 117 | List predicates = newArrayList(filter(predicatesNullAllowed, notNull())); 118 | if (predicates == null || predicates.isEmpty()) { 119 | return null; 120 | } else if (predicates.size() == 1) { 121 | return predicates.get(0); 122 | } else { 123 | return builder.or(toArray(predicates, Predicate.class)); 124 | } 125 | } 126 | 127 | public Predicate stringPredicate(Expression path, Object attrValue, SearchMode searchMode, SearchParameters sp, CriteriaBuilder builder) { 128 | if (sp.isCaseInsensitive()) { 129 | path = builder.lower(path); 130 | attrValue = ((String) attrValue).toLowerCase(LocaleContextHolder.getLocale()); 131 | } 132 | 133 | switch (searchMode != null ? searchMode : sp.getSearchMode()) { 134 | case EQUALS: 135 | return builder.equal(path, attrValue); 136 | case ENDING_LIKE: 137 | return builder.like(path, "%" + attrValue); 138 | case STARTING_LIKE: 139 | return builder.like(path, attrValue + "%"); 140 | case ANYWHERE: 141 | return builder.like(path, "%" + attrValue + "%"); 142 | case LIKE: 143 | return builder.like(path, (String) attrValue); // assume user provide the wild cards 144 | default: 145 | throw new IllegalStateException("expecting a search mode!"); 146 | } 147 | } 148 | 149 | public Predicate stringPredicate(Expression path, Object attrValue, SearchParameters sp, CriteriaBuilder builder) { 150 | return stringPredicate(path, attrValue, null, sp, builder); 151 | } 152 | 153 | /* 154 | * Convert the passed propertyPath into a JPA path. 155 | *

156 | * Note: JPA will do joins if the property is in an associated entity. 157 | */ 158 | @SuppressWarnings("unchecked") 159 | public Path getPath(Root root, List> attributes) { 160 | Path path = root; 161 | for (Attribute attribute : attributes) { 162 | boolean found = false; 163 | if (path instanceof FetchParent) { 164 | for (Fetch fetch : ((FetchParent) path).getFetches()) { 165 | if (attribute.getName().equals(fetch.getAttribute().getName()) && (fetch instanceof Join)) { 166 | path = (Join) fetch; 167 | found = true; 168 | break; 169 | } 170 | } 171 | } 172 | if (!found) { 173 | if (attribute instanceof PluralAttribute) { 174 | path = ((From) path).join(attribute.getName(), JoinType.LEFT); 175 | } else { 176 | path = path.get(attribute.getName()); 177 | } 178 | } 179 | } 180 | return (Path) path; 181 | } 182 | 183 | public void verifyPath(Attribute... path) { 184 | verifyPath(newArrayList(path)); 185 | } 186 | 187 | public void verifyPath(List> path) { 188 | List> attributes = newArrayList(path); 189 | Class from = null; 190 | if (attributes.get(0).isCollection()) { 191 | from = ((PluralAttribute) attributes.get(0)).getElementType().getJavaType(); 192 | } else { 193 | from = attributes.get(0).getJavaType(); 194 | } 195 | attributes.remove(0); 196 | for (Attribute attribute : attributes) { 197 | if (!attribute.getDeclaringType().getJavaType().isAssignableFrom(from)) { 198 | throw new IllegalStateException("Wrong path."); 199 | } 200 | from = attribute.getJavaType(); 201 | } 202 | } 203 | 204 | public > String compositePkPropertyName(T entity) { 205 | Class entityClass = entity.getClass(); 206 | if (compositePkCache.containsKey(entityClass)) { 207 | return compositePkCache.get(entityClass); 208 | } 209 | 210 | for (Method m : entity.getClass().getMethods()) { 211 | if (m.getAnnotation(EmbeddedId.class) != null) { 212 | String propertyName = methodToProperty(m); 213 | compositePkCache.put(entityClass, propertyName); 214 | return propertyName; 215 | } 216 | } 217 | for (Field f : entity.getClass().getFields()) { 218 | if (f.getAnnotation(EmbeddedId.class) != null) { 219 | String propertyName = f.getName(); 220 | compositePkCache.put(entityClass, propertyName); 221 | return propertyName; 222 | } 223 | } 224 | compositePkCache.put(entityClass, null); 225 | return null; 226 | } 227 | 228 | public boolean isPk(ManagedType mt, SingularAttribute attr) { 229 | try { 230 | Method m = MethodUtils.getAccessibleMethod(mt.getJavaType(), "get" + WordUtils.capitalize(attr.getName()), (Class) null); 231 | if (m != null && m.getAnnotation(Id.class) != null) { 232 | return true; 233 | } 234 | 235 | Field field = mt.getJavaType().getField(attr.getName()); 236 | return field.getAnnotation(Id.class) != null; 237 | } catch (Exception e) { 238 | return false; 239 | } 240 | } 241 | 242 | public Object getValue(T example, Attribute attr) { 243 | try { 244 | if (attr.getJavaMember() instanceof Method) { 245 | return ((Method) attr.getJavaMember()).invoke(example); 246 | } else { 247 | return ((Field) attr.getJavaMember()).get(example); 248 | } 249 | } catch (Exception e) { 250 | throw propagate(e); 251 | } 252 | } 253 | 254 | public SingularAttribute attribute(ManagedType mt, Attribute attr) { 255 | return mt.getSingularAttribute(attr.getName(), attr.getJavaType()); 256 | } 257 | 258 | public SingularAttribute stringAttribute(ManagedType mt, Attribute attr) { 259 | return mt.getSingularAttribute(attr.getName(), String.class); 260 | } 261 | 262 | public > boolean hasSimplePk(T entity) { 263 | for (Method m : entity.getClass().getMethods()) { 264 | if (m.getAnnotation(Id.class) != null) { 265 | return true; 266 | } 267 | } 268 | for (Field f : entity.getClass().getFields()) { 269 | if (f.getAnnotation(Id.class) != null) { 270 | return true; 271 | } 272 | } 273 | return false; 274 | } 275 | 276 | public String[] toNames(Attribute... attributes) { 277 | return toNamesList(newArrayList(attributes)).toArray(new String[0]); 278 | } 279 | 280 | public List toNamesList(List> attributes) { 281 | List ret = newArrayList(); 282 | for (Attribute attribute : attributes) { 283 | ret.add(attribute.getName()); 284 | } 285 | return ret; 286 | } 287 | 288 | public String getEntityName(Identifiable entity) { 289 | Entity entityAnnotation = entity.getClass().getAnnotation(Entity.class); 290 | if (isBlank(entityAnnotation.name())) { 291 | return getClassWithoutInitializingProxy(entity).getSimpleName(); 292 | } 293 | return entityAnnotation.name(); 294 | } 295 | 296 | public String methodToProperty(Method m) { 297 | PropertyDescriptor[] pds = PropertyUtils.getPropertyDescriptors(m.getDeclaringClass()); 298 | for (PropertyDescriptor pd : pds) { 299 | if (m.equals(pd.getReadMethod()) || m.equals(pd.getWriteMethod())) { 300 | return pd.getName(); 301 | } 302 | } 303 | return null; 304 | } 305 | 306 | public Object getValueFromField(Field field, Object object) { 307 | boolean accessible = field.isAccessible(); 308 | try { 309 | return getField(field, object); 310 | } finally { 311 | field.setAccessible(accessible); 312 | } 313 | } 314 | 315 | public void applyPagination(Query query, SearchParameters sp) { 316 | if (sp.getFirst() > 0) { 317 | query.setFirstResult(sp.getFirst()); 318 | } 319 | if (sp.getPageSize() > 0) { 320 | query.setMaxResults(sp.getPageSize()); 321 | } else if (sp.getMaxResults() > 0) { 322 | query.setMaxResults(sp.getMaxResults()); 323 | } 324 | } 325 | 326 | @SuppressWarnings("unchecked") 327 | private T getField(Field field, Object target) { 328 | try { 329 | return (T) field.get(target); 330 | } catch (Exception e) { 331 | throw new RuntimeException(e); 332 | } 333 | } 334 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/LabelizedEnum.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | public interface LabelizedEnum { 19 | 20 | /** 21 | * @return The localized label for this enum value. 22 | */ 23 | String getLabel(); 24 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/LuceneQueryBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.apache.lucene.search.Query; 19 | import org.hibernate.search.jpa.FullTextEntityManager; 20 | 21 | import javax.persistence.metamodel.SingularAttribute; 22 | import java.io.Serializable; 23 | import java.util.List; 24 | 25 | public interface LuceneQueryBuilder extends Serializable { 26 | 27 | Query build(FullTextEntityManager fullTextEntityManager, SearchParameters searchParameters, List> availableProperties); 28 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/MetamodelUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import com.google.common.base.Splitter; 19 | import org.springframework.context.annotation.Lazy; 20 | 21 | import javax.inject.Named; 22 | import javax.inject.Singleton; 23 | import javax.persistence.*; 24 | import javax.persistence.metamodel.Attribute; 25 | import javax.persistence.metamodel.PluralAttribute; 26 | import javax.persistence.metamodel.SingularAttribute; 27 | import java.lang.reflect.AccessibleObject; 28 | import java.lang.reflect.Field; 29 | import java.util.Collection; 30 | import java.util.List; 31 | import java.util.Map; 32 | 33 | import static com.google.common.collect.Lists.newArrayList; 34 | import static com.google.common.collect.Maps.newHashMap; 35 | 36 | @Named 37 | @Singleton 38 | @Lazy(false) 39 | public class MetamodelUtil { 40 | private static MetamodelUtil instance; 41 | 42 | public static MetamodelUtil getInstance() { 43 | return instance; 44 | } 45 | 46 | private Map, Class> metamodelCache = newHashMap(); 47 | 48 | public MetamodelUtil() { 49 | instance = this; 50 | } 51 | 52 | public SingularAttribute toAttribute(String property, Class from) { 53 | try { 54 | Class metamodelClass = getCachedClass(from); 55 | Field field = metamodelClass.getField(property); 56 | return (SingularAttribute) field.get(null); 57 | } catch (Exception e) { 58 | throw new IllegalArgumentException(e); 59 | } 60 | } 61 | 62 | public List> toAttributes(String path, Class from) { 63 | try { 64 | List> attributes = newArrayList(); 65 | Class current = from; 66 | for (String pathItem : Splitter.on(".").split(path)) { 67 | Class metamodelClass = getCachedClass(current); 68 | Field field = metamodelClass.getField(pathItem); 69 | Attribute attribute = (Attribute) field.get(null); 70 | attributes.add(attribute); 71 | if (attribute instanceof PluralAttribute) { 72 | current = ((PluralAttribute) attribute).getElementType().getJavaType(); 73 | } else { 74 | current = attribute.getJavaType(); 75 | } 76 | } 77 | return attributes; 78 | } catch (Exception e) { 79 | throw new IllegalArgumentException(e); 80 | } 81 | } 82 | 83 | public String toPath(List> attributes) { 84 | StringBuilder path = new StringBuilder(); 85 | for (Attribute attribute : attributes) { 86 | if (path.length() > 0) { 87 | path.append("."); 88 | } 89 | path.append(attribute.getName()); 90 | } 91 | return path.toString(); 92 | } 93 | 94 | private Class getCachedClass(Class current) throws ClassNotFoundException { 95 | if (metamodelCache.containsKey(current)) { 96 | return metamodelCache.get(current); 97 | } 98 | Class metamodelClass = Class.forName(current.getName() + "_"); 99 | metamodelCache.put(current, metamodelClass); 100 | return metamodelClass; 101 | } 102 | 103 | /** 104 | * Retrieves cascade from metamodel attribute 105 | * 106 | * @param attribute given pluaral attribute 107 | * @return an empty collection if no jpa relation annotation can be found. 108 | */ 109 | public Collection getCascades(PluralAttribute attribute) { 110 | if (attribute.getJavaMember() instanceof AccessibleObject) { 111 | AccessibleObject accessibleObject = (AccessibleObject) attribute.getJavaMember(); 112 | OneToMany oneToMany = accessibleObject.getAnnotation(OneToMany.class); 113 | if (oneToMany != null) { 114 | return newArrayList(oneToMany.cascade()); 115 | } 116 | ManyToMany manyToMany = accessibleObject.getAnnotation(ManyToMany.class); 117 | if (manyToMany != null) { 118 | return newArrayList(manyToMany.cascade()); 119 | } 120 | } 121 | return newArrayList(); 122 | } 123 | 124 | /** 125 | * Retrieves cascade from metamodel attribute on a xToMany relation. 126 | * 127 | * @param attribute given singular attribute 128 | * @return an empty collection if no jpa relation annotation can be found. 129 | */ 130 | public Collection getCascades(SingularAttribute attribute) { 131 | if (attribute.getJavaMember() instanceof AccessibleObject) { 132 | AccessibleObject accessibleObject = (AccessibleObject) attribute.getJavaMember(); 133 | OneToOne oneToOne = accessibleObject.getAnnotation(OneToOne.class); 134 | if (oneToOne != null) { 135 | return newArrayList(oneToOne.cascade()); 136 | } 137 | ManyToOne manyToOne = accessibleObject.getAnnotation(ManyToOne.class); 138 | if (manyToOne != null) { 139 | return newArrayList(manyToOne.cascade()); 140 | } 141 | } 142 | return newArrayList(); 143 | } 144 | 145 | public boolean isOrphanRemoval(PluralAttribute attribute) { 146 | if (attribute.getJavaMember() instanceof AccessibleObject) { 147 | AccessibleObject accessibleObject = (AccessibleObject) attribute.getJavaMember(); 148 | OneToMany oneToMany = accessibleObject.getAnnotation(OneToMany.class); 149 | if (oneToMany != null) { 150 | return oneToMany.orphanRemoval(); 151 | } 152 | } 153 | return true; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/OrderBy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.apache.commons.lang.builder.ToStringBuilder; 19 | 20 | import javax.persistence.metamodel.Attribute; 21 | import java.io.Serializable; 22 | import java.util.List; 23 | 24 | import static com.google.common.base.Preconditions.checkNotNull; 25 | import static com.jaxio.jpa.querybyexample.OrderByDirection.ASC; 26 | import static com.jaxio.jpa.querybyexample.OrderByDirection.DESC; 27 | 28 | /** 29 | * Holder class for search ordering used by the {@link SearchParameters}. 30 | */ 31 | public class OrderBy implements Serializable { 32 | private static final long serialVersionUID = 1L; 33 | private final PathHolder pathHolder; 34 | private OrderByDirection direction = ASC; 35 | 36 | public OrderBy(OrderByDirection direction, Attribute... attributes) { 37 | this.direction = checkNotNull(direction); 38 | this.pathHolder = new PathHolder(checkNotNull(attributes)); 39 | } 40 | 41 | public OrderBy(OrderByDirection direction, String path, Class from) { 42 | this.direction = checkNotNull(direction); 43 | this.pathHolder = new PathHolder(checkNotNull(path), checkNotNull(from)); 44 | } 45 | 46 | public List> getAttributes() { 47 | return pathHolder.getAttributes(); 48 | } 49 | 50 | public String getPath() { 51 | return pathHolder.getPath(); 52 | } 53 | 54 | public OrderByDirection getDirection() { 55 | return direction; 56 | } 57 | 58 | public boolean isOrderDesc() { 59 | return DESC == direction; 60 | } 61 | 62 | @Override 63 | public int hashCode() { 64 | final int prime = 31; 65 | int result = 1; 66 | result = prime * result + ((pathHolder == null) ? 0 : pathHolder.hashCode()); 67 | return result; 68 | } 69 | 70 | @Override 71 | public boolean equals(Object obj) { 72 | if (this == obj) { 73 | return true; 74 | } 75 | if (obj == null) { 76 | return false; 77 | } 78 | if (getClass() != obj.getClass()) { 79 | return false; 80 | } 81 | OrderBy other = (OrderBy) obj; 82 | if (pathHolder == null) { 83 | if (other.pathHolder != null) { 84 | return false; 85 | } 86 | } else if (!pathHolder.equals(other.pathHolder)) { 87 | return false; 88 | } 89 | return true; 90 | } 91 | 92 | @Override 93 | public String toString() { 94 | return ToStringBuilder.reflectionToString(this); 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/OrderByDirection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | public enum OrderByDirection { 19 | ASC, DESC; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/OrderByUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import javax.inject.Inject; 19 | import javax.inject.Named; 20 | import javax.inject.Singleton; 21 | import javax.persistence.criteria.CriteriaBuilder; 22 | import javax.persistence.criteria.Order; 23 | import javax.persistence.criteria.Path; 24 | import javax.persistence.criteria.Root; 25 | import java.util.List; 26 | 27 | import static com.google.common.collect.Lists.newArrayList; 28 | 29 | /** 30 | * Helper to create list of {@link javax.persistence.criteria.Order} out of {@link OrderBy}s. 31 | */ 32 | @Named 33 | @Singleton 34 | public class OrderByUtil { 35 | 36 | @Inject 37 | private JpaUtil jpaUtil; 38 | 39 | public List buildJpaOrders(Iterable orders, Root root, CriteriaBuilder builder, SearchParameters sp) { 40 | List jpaOrders = newArrayList(); 41 | for (OrderBy ob : orders) { 42 | Path path = jpaUtil.getPath(root, ob.getAttributes()); 43 | jpaOrders.add(ob.isOrderDesc() ? builder.desc(path) : builder.asc(path)); 44 | } 45 | return jpaOrders; 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/PathHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import javax.persistence.metamodel.Attribute; 19 | import java.io.Serializable; 20 | import java.util.List; 21 | 22 | import static com.google.common.base.Preconditions.checkNotNull; 23 | import static com.google.common.collect.Lists.newArrayList; 24 | 25 | /** 26 | * Holder class for path used by the {@link OrderBy}, {@link PropertySelector}, {@link TermSelector} and {@link SearchParameters}. 27 | */ 28 | public class PathHolder implements Serializable { 29 | private static final long serialVersionUID = 1L; 30 | private final String path; 31 | private final Class from; 32 | private transient List> attributes; 33 | 34 | public PathHolder(Attribute... attributes) { 35 | this(newArrayList(attributes)); 36 | } 37 | 38 | public PathHolder(List> attributes) { 39 | JpaUtil.getInstance().verifyPath(checkNotNull(attributes)); 40 | this.attributes = newArrayList(attributes); 41 | this.path = MetamodelUtil.getInstance().toPath(attributes); 42 | this.from = attributes.get(0).getDeclaringType().getJavaType(); 43 | } 44 | 45 | public PathHolder(String path, Class from) { 46 | this.path = path; 47 | this.from = from; 48 | // to verify path 49 | getAttributes(); 50 | } 51 | 52 | public List> getAttributes() { 53 | if (attributes == null) { 54 | attributes = MetamodelUtil.getInstance().toAttributes(path, from); 55 | } 56 | return attributes; 57 | } 58 | 59 | public String getPath() { 60 | return path; 61 | } 62 | 63 | @Override 64 | public int hashCode() { 65 | final int prime = 31; 66 | int result = 1; 67 | result = prime * result + ((path == null) ? 0 : path.hashCode()); 68 | return result; 69 | } 70 | 71 | @Override 72 | public boolean equals(Object obj) { 73 | if (this == obj) { 74 | return true; 75 | } 76 | if (obj == null) { 77 | return false; 78 | } 79 | if (getClass() != obj.getClass()) { 80 | return false; 81 | } 82 | PathHolder other = (PathHolder) obj; 83 | if (path == null) { 84 | if (other.path != null) { 85 | return false; 86 | } 87 | } else if (!path.equals(other.path)) { 88 | return false; 89 | } 90 | return true; 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/PropertySelector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import org.apache.commons.lang.builder.ToStringBuilder; 19 | 20 | import javax.persistence.metamodel.Attribute; 21 | import java.io.Serializable; 22 | import java.util.List; 23 | 24 | import static com.google.common.base.Preconditions.checkNotNull; 25 | import static com.google.common.collect.Lists.newArrayList; 26 | 27 | /** 28 | * Used to construct OR predicate for a property value. In other words you can search 29 | * all entities E having a given property set to one of the selected values. 30 | */ 31 | public class PropertySelector implements Serializable { 32 | /* 33 | * PropertySelector builder 34 | */ 35 | public static PropertySelector newPropertySelector(Attribute... fields) { 36 | return new PropertySelector(checkNotNull(fields)); 37 | } 38 | 39 | /* 40 | * PropertySelector builder 41 | */ 42 | public static PropertySelector newPropertySelector(String path, Class from) { 43 | return new PropertySelector(path, from); 44 | } 45 | 46 | /* 47 | * PropertySelector builder 48 | */ 49 | public static PropertySelector newPropertySelector(boolean orMode, Attribute... fields) { 50 | PropertySelector ps = new PropertySelector(checkNotNull(fields)); 51 | return ps.orMode(orMode); 52 | } 53 | 54 | private static final long serialVersionUID = 1L; 55 | 56 | private final PathHolder pathHolder; 57 | private List selected = newArrayList(); 58 | private SearchMode searchMode; // for string property only. 59 | private Boolean notIncludingNull; 60 | private boolean orMode = true; 61 | 62 | public PropertySelector(Attribute... attributes) { 63 | this.pathHolder = new PathHolder(checkNotNull(attributes)); 64 | } 65 | 66 | public PropertySelector(String path, Class from) { 67 | this.pathHolder = new PathHolder(path, from); 68 | } 69 | 70 | public List> getAttributes() { 71 | return pathHolder.getAttributes(); 72 | } 73 | 74 | public boolean isNotIncludingNullSet() { 75 | return notIncludingNull != null; 76 | } 77 | 78 | public Boolean isNotIncludingNull() { 79 | return notIncludingNull; 80 | } 81 | 82 | public PropertySelector withoutNull() { 83 | this.notIncludingNull = true; 84 | return this; 85 | } 86 | 87 | /* 88 | * Get the possible candidates for property. 89 | */ 90 | public List getSelected() { 91 | return selected; 92 | } 93 | 94 | public PropertySelector add(F object) { 95 | this.selected.add(object); 96 | return this; 97 | } 98 | 99 | /* 100 | * Set the possible candidates for property. 101 | */ 102 | public void setSelected(List selected) { 103 | this.selected = selected; 104 | } 105 | 106 | public PropertySelector selected(F... selected) { 107 | setSelected(newArrayList(selected)); 108 | return this; 109 | } 110 | 111 | public boolean isNotEmpty() { 112 | return selected != null && !selected.isEmpty(); 113 | } 114 | 115 | public void clearSelected() { 116 | if (selected != null) { 117 | selected.clear(); 118 | } 119 | } 120 | 121 | @SuppressWarnings("unchecked") 122 | public void setValue(F value) { 123 | this.selected = newArrayList(value); 124 | } 125 | 126 | public F getValue() { 127 | return isNotEmpty() ? selected.get(0) : null; 128 | } 129 | 130 | public boolean isBoolean() { 131 | return isType(Boolean.class); 132 | } 133 | 134 | public boolean isLabelizedEnum() { 135 | return isType(LabelizedEnum.class); 136 | } 137 | 138 | public boolean isString() { 139 | return isType(String.class); 140 | } 141 | 142 | public boolean isNumber() { 143 | return isType(Number.class); 144 | } 145 | 146 | public boolean isType(Class type) { 147 | return type.isAssignableFrom(getAttributes().get(getAttributes().size() - 1).getJavaType()); 148 | } 149 | 150 | public SearchMode getSearchMode() { 151 | return searchMode; 152 | } 153 | 154 | /* 155 | * In case, the field's type is a String, you can set a searchMode to use. It is null by default. 156 | */ 157 | public void setSearchMode(SearchMode searchMode) { 158 | this.searchMode = searchMode; 159 | } 160 | 161 | public PropertySelector searchMode(SearchMode searchMode) { 162 | setSearchMode(searchMode); 163 | return this; 164 | } 165 | 166 | public boolean isOrMode() { 167 | return orMode; 168 | } 169 | 170 | public void setOrMode(boolean orMode) { 171 | this.orMode = orMode; 172 | } 173 | 174 | public PropertySelector orMode(boolean orMode) { 175 | setOrMode(orMode); 176 | return this; 177 | } 178 | 179 | @Override 180 | public String toString() { 181 | return ToStringBuilder.reflectionToString(this); 182 | } 183 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/Range.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import javax.persistence.metamodel.Attribute; 19 | import java.io.Serializable; 20 | import java.util.List; 21 | 22 | /** 23 | * Range support for {@link Comparable} types. 24 | */ 25 | @SuppressWarnings("rawtypes") 26 | public class Range implements Serializable { 27 | 28 | /* 29 | * Range builder 30 | */ 31 | public static Range newRange(Attribute... fields) { 32 | return new Range(fields); 33 | } 34 | 35 | private static final long serialVersionUID = 1L; 36 | 37 | private final PathHolder pathHolder; 38 | private D from; 39 | private D to; 40 | private Boolean includeNull; 41 | 42 | /** 43 | * Constructs a new Range with no boundaries and no restrictions on field's nullability. 44 | * 45 | * @param attributes the path to the attribute of an existing entity. 46 | */ 47 | public Range(Attribute... attributes) { 48 | pathHolder = new PathHolder(attributes); 49 | } 50 | 51 | /** 52 | * Constructs a new Range. 53 | * 54 | * @param from the lower boundary of this range. Null means no lower boundary. 55 | * @param to the upper boundary of this range. Null means no upper boundary. 56 | * @param attributes the path to the attribute of an existing entity. 57 | */ 58 | public Range(D from, D to, Attribute... attributes) { 59 | this(attributes); 60 | this.from = from; 61 | this.to = to; 62 | } 63 | 64 | /** 65 | * Constructs a new Range. 66 | * 67 | * @param from the lower boundary of this range. Null means no lower boundary. 68 | * @param to the upper boundary of this range. Null means no upper boundary. 69 | * @param includeNull tells whether null should be filtered out or not. 70 | * @param attributes the path to the attribute of an existing entity. 71 | */ 72 | public Range(D from, D to, Boolean includeNull, Attribute... attributes) { 73 | this(from, to, attributes); 74 | this.includeNull = includeNull; 75 | } 76 | 77 | /* 78 | * Constructs a new Range by copy. 79 | */ 80 | public Range(Range other) { 81 | this.pathHolder = other.pathHolder; 82 | this.from = other.from; 83 | this.to = other.to; 84 | this.includeNull = other.includeNull; 85 | } 86 | 87 | /** 88 | * @return the entity's attribute this Range refers to. 89 | */ 90 | public List> getAttributes() { 91 | return pathHolder.getAttributes(); 92 | } 93 | 94 | /** 95 | * @return the lower range boundary or null for unbound lower range. 96 | */ 97 | public D getFrom() { 98 | return from; 99 | } 100 | 101 | /* 102 | * Sets the lower range boundary. Accepts null for unbound lower range. 103 | */ 104 | public void setFrom(D from) { 105 | this.from = from; 106 | } 107 | 108 | public Range from(D from) { 109 | setFrom(from); 110 | return this; 111 | } 112 | 113 | public boolean isFromSet() { 114 | return getFrom() != null; 115 | } 116 | 117 | /** 118 | * @return the upper range boundary or null for unbound upper range. 119 | */ 120 | public D getTo() { 121 | return to; 122 | } 123 | 124 | public Range to(D to) { 125 | setTo(to); 126 | return this; 127 | } 128 | 129 | /* 130 | * Sets the upper range boundary. Accepts null for unbound upper range. 131 | */ 132 | public void setTo(D to) { 133 | this.to = to; 134 | } 135 | 136 | public boolean isToSet() { 137 | return getTo() != null; 138 | } 139 | 140 | public void setIncludeNull(Boolean includeNull) { 141 | this.includeNull = includeNull; 142 | } 143 | 144 | public Range includeNull(Boolean includeNull) { 145 | setIncludeNull(includeNull); 146 | return this; 147 | } 148 | 149 | public Boolean getIncludeNull() { 150 | return includeNull; 151 | } 152 | 153 | public boolean isIncludeNullSet() { 154 | return includeNull != null; 155 | } 156 | 157 | public boolean isBetween() { 158 | return isFromSet() && isToSet(); 159 | } 160 | 161 | public boolean isSet() { 162 | return isFromSet() || isToSet() || isIncludeNullSet(); 163 | } 164 | 165 | @SuppressWarnings("unchecked") 166 | public boolean isValid() { 167 | if (isBetween()) { 168 | return getFrom().compareTo(getTo()) <= 0; 169 | } 170 | return true; 171 | } 172 | 173 | public void resetRange() { 174 | from = null; 175 | to = null; 176 | includeNull = null; 177 | } 178 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/RepositoryLocator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import javax.inject.Inject; 19 | import javax.inject.Named; 20 | import javax.inject.Singleton; 21 | import java.io.Serializable; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | import static com.google.common.collect.Maps.newHashMap; 26 | import static org.hibernate.proxy.HibernateProxyHelper.getClassWithoutInitializingProxy; 27 | 28 | @Named 29 | @Singleton 30 | public class RepositoryLocator { 31 | private Map, GenericRepository> repositories = newHashMap(); 32 | 33 | @Inject 34 | void buildCache(List> registredRepositories) { 35 | for (GenericRepository repository : registredRepositories) { 36 | repositories.put(repository.getType(), repository); 37 | } 38 | } 39 | 40 | @SuppressWarnings("unchecked") 41 | public > GenericRepository getRepository(Class clazz) { 42 | return (GenericRepository) repositories.get(clazz); 43 | } 44 | 45 | @SuppressWarnings("unchecked") 46 | public > GenericRepository getRepository(E entity) { 47 | return (GenericRepository) repositories.get(getClassWithoutInitializingProxy(entity)); 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/SearchMode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | /** 19 | * Static values to use in conjunction with {@link SearchParameters} object. 20 | * It maps the kind of search you can do in SQL. 21 | */ 22 | public enum SearchMode { 23 | /** 24 | * Match exactly the properties 25 | */ 26 | EQUALS("eq"), 27 | /** 28 | * Activates LIKE search and add a '%' prefix and suffix before searching. 29 | */ 30 | ANYWHERE("any"), 31 | /** 32 | * Activate LIKE search and add a '%' prefix before searching. 33 | */ 34 | STARTING_LIKE("sl"), 35 | /** 36 | * Activate LIKE search. User provides the wildcard. 37 | */ 38 | LIKE("li"), 39 | /** 40 | * Activate LIKE search and add a '%' suffix before searching. 41 | */ 42 | ENDING_LIKE("el"); 43 | 44 | private final String code; 45 | 46 | SearchMode(String code) { 47 | this.code = code; 48 | } 49 | 50 | public String getCode() { 51 | return code; 52 | } 53 | 54 | public static final SearchMode convert(String code) { 55 | for (SearchMode searchMode : SearchMode.values()) { 56 | if (searchMode.getCode().equals(code)) { 57 | return searchMode; 58 | } 59 | } 60 | 61 | return EQUALS; // default 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/SearchParameters.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import com.google.common.base.Function; 19 | import org.apache.commons.lang.builder.ToStringBuilder; 20 | 21 | import javax.persistence.metamodel.Attribute; 22 | import javax.persistence.metamodel.SingularAttribute; 23 | import java.io.Serializable; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Set; 27 | 28 | import static com.google.common.base.Preconditions.checkNotNull; 29 | import static com.google.common.collect.Lists.newArrayList; 30 | import static com.google.common.collect.Lists.transform; 31 | import static com.google.common.collect.Maps.newHashMap; 32 | import static com.google.common.collect.Sets.newHashSet; 33 | import static com.jaxio.jpa.querybyexample.PropertySelector.newPropertySelector; 34 | import static org.apache.commons.lang.StringUtils.isNotBlank; 35 | 36 | /* 37 | * The SearchParameters is used to pass search parameters to the DAO layer. 38 | *

39 | * Its usage keeps 'find' method signatures in the DAO/Service layer simple. 40 | *

41 | * A SearchParameters helps you drive your search in the following areas: 42 | *

    43 | *
  • Configure the search mode (EQUALS, LIKE, ...)
  • 44 | *
  • Pagination: it allows you to limit your search results to a specific range.
  • 45 | *
  • Allow you to specify ORDER BY and ASC/DESC
  • 46 | *
  • Enable/disable case sensitivity
  • 47 | *
  • Enable/disable 2d level cache
  • 48 | *
  • LIKE search against all string values: simply set the searchPattern property
  • 49 | *
  • Named query: if you set a named query it will be executed. Named queries can be defined in annotation or src/main/resources/META-INF/orm.xml
  • 50 | *
  • FullTextSearch: simply set the term property (requires Hibernate Search)
  • 51 | *
52 | *

53 | * Note : All requests are limited to a maximum number of elements to prevent resource exhaustion. 54 | * 55 | * @see SearchMode 56 | * @see OrderBy 57 | * @see Range 58 | * @see PropertySelector 59 | */ 60 | public class SearchParameters implements Serializable { 61 | private static final long serialVersionUID = 1L; 62 | 63 | private SearchMode searchMode = SearchMode.EQUALS; 64 | private boolean andMode = true; 65 | 66 | // named query related 67 | private String namedQuery; 68 | private Map parameters = newHashMap(); 69 | 70 | private Set orders = newHashSet(); 71 | 72 | // technical parameters 73 | private boolean caseSensitive = true; 74 | 75 | // pagination 76 | private int maxResults = -1; 77 | private int first = 0; 78 | private int pageSize = 0; 79 | 80 | // fetches 81 | private Set fetches = newHashSet(); 82 | 83 | // ranges 84 | private List> ranges = newArrayList(); 85 | 86 | // property selectors 87 | private List> properties = newArrayList(); 88 | 89 | // pattern to match against all strings. 90 | private String searchPattern; 91 | 92 | // hibernate search terms 93 | private List terms = newArrayList(); 94 | private Float searchSimilarity = 0.5f; 95 | private LuceneQueryBuilder luceneQueryBuilder = new DefaultLuceneQueryBuilder(); 96 | 97 | // Warn: before enabling cache for queries, 98 | // check this: https://hibernate.atlassian.net/browse/HHH-1523 99 | private Boolean cacheable = false; 100 | private String cacheRegion; 101 | 102 | // extra parameters 103 | private Map extraParameters = newHashMap(); 104 | 105 | private boolean useAndInXToMany = true; 106 | 107 | private boolean useDistinct = false; 108 | 109 | // ----------------------------------- 110 | // SearchMode 111 | // ----------------------------------- 112 | 113 | /* 114 | * Fluently set the @{link SearchMode}. It defaults to EQUALS. 115 | * 116 | * @param searchMode searchmode 117 | * @see SearchMode#EQUALS 118 | */ 119 | public void setSearchMode(SearchMode searchMode) { 120 | this.searchMode = checkNotNull(searchMode); 121 | } 122 | 123 | /* 124 | * Return the @{link SearchMode}. It defaults to EQUALS. 125 | * 126 | * @see SearchMode#EQUALS 127 | */ 128 | public SearchMode getSearchMode() { 129 | return searchMode; 130 | } 131 | 132 | public boolean is(SearchMode searchMode) { 133 | return getSearchMode() == searchMode; 134 | } 135 | 136 | /* 137 | * Fluently set the @{link SearchMode}. It defaults to EQUALS. 138 | * 139 | * @param searchMode searchmode 140 | * @see SearchMode#EQUALS 141 | */ 142 | public SearchParameters searchMode(SearchMode searchMode) { 143 | setSearchMode(searchMode); 144 | return this; 145 | } 146 | 147 | /* 148 | * Use the EQUALS @{link SearchMode}. 149 | * 150 | * @param searchMode searchmode 151 | * @see SearchMode#EQUALS 152 | */ 153 | public SearchParameters equals() { 154 | return searchMode(SearchMode.EQUALS); 155 | } 156 | 157 | /* 158 | * Use the ANYWHERE @{link SearchMode}. 159 | * 160 | * @param searchMode searchmode 161 | * @see SearchMode#ANYWHERE 162 | */ 163 | public SearchParameters anywhere() { 164 | return searchMode(SearchMode.ANYWHERE); 165 | } 166 | 167 | /* 168 | * Use the STARTING_LIKE @{link SearchMode}. 169 | * 170 | * @see SearchMode#STARTING_LIKE 171 | */ 172 | public SearchParameters startingLike() { 173 | return searchMode(SearchMode.STARTING_LIKE); 174 | } 175 | 176 | /* 177 | * Use the LIKE @{link SearchMode}. 178 | * 179 | * @see SearchMode#LIKE 180 | */ 181 | public SearchParameters like() { 182 | return searchMode(SearchMode.LIKE); 183 | } 184 | 185 | /* 186 | * Use the ENDING_LIKE @{link SearchMode}. 187 | * 188 | * @see SearchMode#ENDING_LIKE 189 | */ 190 | public SearchParameters endingLike() { 191 | return searchMode(SearchMode.ENDING_LIKE); 192 | } 193 | 194 | // ----------------------------------- 195 | // Predicate mode 196 | // ----------------------------------- 197 | 198 | public void setAndMode(boolean andMode) { 199 | this.andMode = andMode; 200 | } 201 | 202 | public boolean isAndMode() { 203 | return andMode; 204 | } 205 | 206 | /* 207 | * use and to build the final predicate 208 | */ 209 | public SearchParameters andMode() { 210 | setAndMode(true); 211 | return this; 212 | } 213 | 214 | /* 215 | * use or to build the final predicate 216 | */ 217 | public SearchParameters orMode() { 218 | setAndMode(false); 219 | return this; 220 | } 221 | 222 | // ----------------------------------- 223 | // Named query support 224 | // ----------------------------------- 225 | 226 | /* 227 | * Returns true if a named query has been set, false otherwise. When it returns true, the DAO layer will call the namedQuery. 228 | */ 229 | public boolean hasNamedQuery() { 230 | return isNotBlank(namedQuery); 231 | } 232 | 233 | /* 234 | * Set the named query to be used by the DAO layer. Null by default. 235 | */ 236 | public void setNamedQuery(String namedQuery) { 237 | this.namedQuery = namedQuery; 238 | } 239 | 240 | /* 241 | * Return the name of the named query to be used by the DAO layer. 242 | */ 243 | public String getNamedQuery() { 244 | return namedQuery; 245 | } 246 | 247 | /* 248 | * Set the parameters for the named query. 249 | */ 250 | public void setNamedQueryParameters(Map parameters) { 251 | this.parameters = checkNotNull(parameters); 252 | } 253 | 254 | /* 255 | * Set the parameters for the named query. 256 | */ 257 | public void addNamedQueryParameter(String name, Object value) { 258 | parameters.put(checkNotNull(name), checkNotNull(value)); 259 | } 260 | 261 | /* 262 | * The parameters associated with the named query, if any. 263 | */ 264 | public Map getNamedQueryParameters() { 265 | return parameters; 266 | } 267 | 268 | /* 269 | * Return the value of the given parameter name. 270 | */ 271 | public Object getNamedQueryParameter(String parameterName) { 272 | return parameters.get(checkNotNull(parameterName)); 273 | } 274 | 275 | /* 276 | * Fluently set the named query to be used by the DAO layer. Null by default. 277 | */ 278 | public SearchParameters namedQuery(String namedQuery) { 279 | setNamedQuery(namedQuery); 280 | return this; 281 | } 282 | 283 | /* 284 | * Fluently set the parameters for the named query. 285 | */ 286 | public SearchParameters namedQueryParameters(Map parameters) { 287 | setNamedQueryParameters(parameters); 288 | return this; 289 | } 290 | 291 | /* 292 | * Fluently set the parameter for the named query. 293 | */ 294 | public SearchParameters namedQueryParameter(String name, Object value) { 295 | addNamedQueryParameter(name, value); 296 | return this; 297 | } 298 | 299 | // ----------------------------------- 300 | // Search pattern support 301 | // ----------------------------------- 302 | 303 | /* 304 | * When it returns true, it indicates to the DAO layer to use the given searchPattern on all string properties. 305 | */ 306 | public boolean hasSearchPattern() { 307 | return isNotBlank(searchPattern); 308 | } 309 | 310 | /* 311 | * Set the pattern which may contains wildcards (ex: e%r%ka ). 312 | *

313 | * The given searchPattern is used by the DAO layer on all string properties. Null by default. 314 | */ 315 | public void setSearchPattern(String searchPattern) { 316 | this.searchPattern = searchPattern; 317 | } 318 | 319 | public SearchParameters searchPattern(String searchPattern) { 320 | setSearchPattern(searchPattern); 321 | return this; 322 | } 323 | 324 | public String getSearchPattern() { 325 | return searchPattern; 326 | } 327 | 328 | // ----------------------------------- 329 | // Terms support (hibernate search) 330 | // ----------------------------------- 331 | 332 | public void addTerm(TermSelector term) { 333 | terms.add(checkNotNull(term)); 334 | } 335 | 336 | public List getTerms() { 337 | return terms; 338 | } 339 | 340 | public boolean hasTerms() { 341 | return !terms.isEmpty(); 342 | } 343 | 344 | public Float getSearchSimilarity() { 345 | return searchSimilarity; 346 | } 347 | 348 | public void setSearchSimilarity(Float searchSimilarity) { 349 | this.searchSimilarity = searchSimilarity; 350 | } 351 | 352 | public LuceneQueryBuilder getLuceneQueryBuilder() { 353 | return luceneQueryBuilder; 354 | } 355 | 356 | public void setLuceneQueryBuilder(LuceneQueryBuilder luceneQueryBuilder) { 357 | this.luceneQueryBuilder = checkNotNull(luceneQueryBuilder); 358 | } 359 | 360 | public SearchParameters term(TermSelector... terms) { 361 | for (TermSelector term : checkNotNull(terms)) { 362 | addTerm(term); 363 | } 364 | return this; 365 | } 366 | 367 | public SearchParameters term(String... selected) { 368 | return term(new TermSelector().selected(selected)); 369 | } 370 | 371 | public SearchParameters term(SingularAttribute attribute, String... selected) { 372 | return term(new TermSelector(attribute).selected(selected)); 373 | } 374 | 375 | /* 376 | * Specify the similarity for the indexed properties, {@link #searchSimilarity} is between 0f and 1f 377 | */ 378 | public SearchParameters searchSimilarity(Float searchSimilarity) { 379 | setSearchSimilarity(searchSimilarity); 380 | return this; 381 | } 382 | 383 | public SearchParameters luceneQueryBuilder(LuceneQueryBuilder luceneQueryBuilder) { 384 | setLuceneQueryBuilder(luceneQueryBuilder); 385 | return this; 386 | } 387 | 388 | // ----------------------------------- 389 | // Case sensitiveness support 390 | // ----------------------------------- 391 | 392 | /* 393 | * Set the case sensitiveness. Defaults to false. 394 | * 395 | * @param caseSensitive caseSensitive 396 | */ 397 | public void setCaseSensitive(boolean caseSensitive) { 398 | this.caseSensitive = caseSensitive; 399 | } 400 | 401 | public boolean isCaseSensitive() { 402 | return caseSensitive; 403 | } 404 | 405 | public boolean isCaseInsensitive() { 406 | return !caseSensitive; 407 | } 408 | 409 | /* 410 | * Fluently set the case sensitiveness. Defaults to false. 411 | * 412 | * @param caseSensitive caseSensitive 413 | */ 414 | public SearchParameters caseSensitive(boolean caseSensitive) { 415 | setCaseSensitive(caseSensitive); 416 | return this; 417 | } 418 | 419 | /* 420 | * Fluently set the case sensitiveness to true. 421 | */ 422 | public SearchParameters caseSensitive() { 423 | return caseSensitive(true); 424 | } 425 | 426 | /* 427 | * Fluently set the case sensitiveness to false. 428 | */ 429 | public SearchParameters caseInsensitive() { 430 | return caseSensitive(false); 431 | } 432 | 433 | // ----------------------------------- 434 | // Order by support 435 | // ----------------------------------- 436 | 437 | public List getOrders() { 438 | return newArrayList(orders); 439 | } 440 | 441 | public void addOrderBy(OrderBy orderBy) { 442 | if (!orders.add(checkNotNull(orderBy))) { 443 | throw new IllegalArgumentException("Duplicate orderBy: '" + orderBy + "'."); 444 | } 445 | } 446 | 447 | public boolean hasOrders() { 448 | return !orders.isEmpty(); 449 | } 450 | 451 | public SearchParameters orderBy(OrderBy... orderBys) { 452 | for (OrderBy orderBy : checkNotNull(orderBys)) { 453 | addOrderBy(orderBy); 454 | } 455 | return this; 456 | } 457 | 458 | public SearchParameters asc(Attribute... attributes) { 459 | return orderBy(new OrderBy(OrderByDirection.ASC, attributes)); 460 | } 461 | 462 | public SearchParameters desc(Attribute... attributes) { 463 | return orderBy(new OrderBy(OrderByDirection.DESC, attributes)); 464 | } 465 | 466 | public SearchParameters orderBy(OrderByDirection orderByDirection, Attribute... attributes) { 467 | return orderBy(new OrderBy(orderByDirection, attributes)); 468 | } 469 | 470 | public SearchParameters asc(String property, Class from) { 471 | return orderBy(new OrderBy(OrderByDirection.ASC, property, from)); 472 | } 473 | 474 | public SearchParameters desc(String property, Class from) { 475 | return orderBy(new OrderBy(OrderByDirection.DESC, property, from)); 476 | } 477 | 478 | public SearchParameters orderBy(OrderByDirection orderByDirection, String property, Class from) { 479 | return orderBy(new OrderBy(orderByDirection, property, from)); 480 | } 481 | 482 | // ----------------------------------- 483 | // Search by range support 484 | // ----------------------------------- 485 | 486 | public List> getRanges() { 487 | return ranges; 488 | } 489 | 490 | public void addRange(Range range) { 491 | ranges.add(checkNotNull(range)); 492 | } 493 | 494 | public boolean hasRanges() { 495 | return !ranges.isEmpty(); 496 | } 497 | 498 | public SearchParameters range(Range... ranges) { 499 | for (Range range : checkNotNull(ranges)) { 500 | addRange(range); 501 | } 502 | return this; 503 | } 504 | 505 | public > SearchParameters range(D from, D to, Attribute... attributes) { 506 | return range(new Range(from, to, attributes)); 507 | } 508 | 509 | // ----------------------------------- 510 | // Search by property selector support 511 | // ----------------------------------- 512 | 513 | public List> getProperties() { 514 | return properties; 515 | } 516 | 517 | public void addProperty(PropertySelector propertySelector) { 518 | properties.add(checkNotNull(propertySelector)); 519 | } 520 | 521 | public boolean hasProperties() { 522 | return !properties.isEmpty(); 523 | } 524 | 525 | public SearchParameters property(PropertySelector... propertySelectors) { 526 | for (PropertySelector propertySelector : checkNotNull(propertySelectors)) { 527 | addProperty(propertySelector); 528 | } 529 | return this; 530 | } 531 | 532 | public SearchParameters property(Attribute fields, F... selected) { 533 | return property(newPropertySelector(fields).selected(selected)); 534 | } 535 | 536 | // ----------------------------------- 537 | // Pagination support 538 | // ----------------------------------- 539 | 540 | /* 541 | * Set the maximum number of results to retrieve. Pass -1 for no limits. 542 | */ 543 | public void setMaxResults(int maxResults) { 544 | this.maxResults = maxResults; 545 | } 546 | 547 | public int getMaxResults() { 548 | return maxResults; 549 | } 550 | 551 | /* 552 | * Set the position of the first result to retrieve. 553 | * 554 | * @param first position of the first result, numbered from 0 555 | */ 556 | public void setFirst(int first) { 557 | this.first = first; 558 | } 559 | 560 | public int getFirst() { 561 | return first; 562 | } 563 | 564 | /* 565 | * Set the page size, that is the maximum number of result to retrieve. 566 | */ 567 | public void setPageSize(int pageSize) { 568 | this.pageSize = pageSize; 569 | } 570 | 571 | public int getPageSize() { 572 | return pageSize; 573 | } 574 | 575 | public SearchParameters maxResults(int maxResults) { 576 | setMaxResults(maxResults); 577 | return this; 578 | } 579 | 580 | public SearchParameters noLimit() { 581 | setMaxResults(-1); 582 | return this; 583 | } 584 | 585 | public SearchParameters limitBroadSearch() { 586 | setMaxResults(500); 587 | return this; 588 | } 589 | 590 | public SearchParameters first(int first) { 591 | setFirst(first); 592 | return this; 593 | } 594 | 595 | public SearchParameters pageSize(int pageSize) { 596 | setPageSize(pageSize); 597 | return this; 598 | } 599 | 600 | // ----------------------------------------- 601 | // Fetch associated entity using a LEFT Join 602 | // ----------------------------------------- 603 | 604 | /* 605 | * Returns the attributes (x-to-one association) which must be fetched with a left join. 606 | */ 607 | public List>> getFetches() { 608 | return transform(newArrayList(fetches), new Function>>() { 609 | public List> apply(PathHolder input) { 610 | return input.getAttributes(); 611 | } 612 | }); 613 | } 614 | 615 | public boolean hasFetches() { 616 | return !fetches.isEmpty(); 617 | } 618 | 619 | /* 620 | * The given attribute (x-to-one association) will be fetched with a left join. 621 | */ 622 | public void addFetch(Attribute... attributes) { 623 | addFetch(newArrayList(attributes)); 624 | } 625 | 626 | public void addFetch(List> attributes) { 627 | fetches.add(new PathHolder(attributes)); 628 | } 629 | 630 | /* 631 | * Fluently set the fetch attribute 632 | */ 633 | public SearchParameters fetch(Attribute... attributes) { 634 | fetch(newArrayList(attributes)); 635 | return this; 636 | } 637 | 638 | /* 639 | * Fluently set the fetch attribute 640 | */ 641 | public SearchParameters fetch(List> attributes) { 642 | addFetch(attributes); 643 | return this; 644 | } 645 | 646 | // ----------------------------------- 647 | // Caching support 648 | // ----------------------------------- 649 | 650 | /* 651 | * Default to false. Please read https://hibernate.atlassian.net/browse/HHH-1523 before using cache. 652 | */ 653 | public void setCacheable(boolean cacheable) { 654 | this.cacheable = cacheable; 655 | } 656 | 657 | public boolean isCacheable() { 658 | return cacheable; 659 | } 660 | 661 | public boolean hasCacheRegion() { 662 | return isNotBlank(cacheRegion); 663 | } 664 | 665 | public void setCacheRegion(String cacheRegion) { 666 | this.cacheRegion = cacheRegion; 667 | } 668 | 669 | public String getCacheRegion() { 670 | return cacheRegion; 671 | } 672 | 673 | public SearchParameters cacheable(boolean cacheable) { 674 | setCacheable(cacheable); 675 | return this; 676 | } 677 | 678 | public SearchParameters enableCache() { 679 | setCacheable(true); 680 | return this; 681 | } 682 | 683 | public SearchParameters disableCache() { 684 | setCacheable(false); 685 | return this; 686 | } 687 | 688 | public SearchParameters cacheRegion(String cacheRegion) { 689 | setCacheRegion(checkNotNull(cacheRegion)); 690 | return this; 691 | } 692 | 693 | // ----------------------------------- 694 | // Extra parameters 695 | // ----------------------------------- 696 | 697 | /* 698 | * Set additionnal parameters. 699 | */ 700 | public void setExtraParameters(Map extraParameters) { 701 | this.extraParameters = extraParameters; 702 | } 703 | 704 | public Map getExtraParameters() { 705 | return extraParameters; 706 | } 707 | 708 | /* 709 | * add additionnal parameter. 710 | */ 711 | public SearchParameters addExtraParameter(String key, Object o) { 712 | extraParameters.put(checkNotNull(key), o); 713 | return this; 714 | } 715 | 716 | /* 717 | * get additionnal parameter. 718 | */ 719 | @SuppressWarnings("unchecked") 720 | public T getExtraParameter(String key) { 721 | return (T) extraParameters.get(key); 722 | } 723 | 724 | // ----------------------------------- 725 | // Use and in XToMany Search 726 | // ----------------------------------- 727 | 728 | public void setUseAndInXToMany(boolean useAndInXToMany) { 729 | this.useAndInXToMany = useAndInXToMany; 730 | } 731 | 732 | public boolean getUseAndInXToMany() { 733 | return useAndInXToMany; 734 | } 735 | 736 | public SearchParameters useOrInXToMany() { 737 | return useAndInXToMany(false); 738 | } 739 | 740 | public SearchParameters useAndInXToMany() { 741 | return useAndInXToMany(true); 742 | } 743 | 744 | public SearchParameters useAndInXToMany(boolean xToManyAndMode) { 745 | setUseAndInXToMany(xToManyAndMode); 746 | return this; 747 | } 748 | 749 | // ----------------------------------- 750 | // Distinct 751 | // ----------------------------------- 752 | 753 | public void setDistinct(boolean useDistinct) { 754 | this.useDistinct = useDistinct; 755 | } 756 | 757 | public boolean getDistinct() { 758 | return useDistinct; 759 | } 760 | 761 | public SearchParameters distinct(boolean useDistinct) { 762 | setDistinct(useDistinct); 763 | return this; 764 | } 765 | 766 | public SearchParameters distinct() { 767 | return distinct(true); 768 | } 769 | 770 | @Override 771 | public String toString() { 772 | return ToStringBuilder.reflectionToString(this); 773 | } 774 | } -------------------------------------------------------------------------------- /src/main/java/com/jaxio/jpa/querybyexample/TermSelector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 JAXIO http://www.jaxio.com 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 | * http://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 | package com.jaxio.jpa.querybyexample; 17 | 18 | import javax.persistence.metamodel.SingularAttribute; 19 | import java.io.Serializable; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | 23 | import static com.google.common.collect.Lists.newArrayList; 24 | import static org.apache.commons.lang.StringUtils.isNotBlank; 25 | 26 | public class TermSelector implements Serializable { 27 | private static final long serialVersionUID = 1L; 28 | private final PathHolder pathHolder; 29 | private List selected = newArrayList(); 30 | private boolean orMode = true; 31 | 32 | public TermSelector() { 33 | this.pathHolder = null; 34 | } 35 | 36 | public TermSelector(SingularAttribute attribute) { 37 | this.pathHolder = new PathHolder(attribute); 38 | } 39 | 40 | public SingularAttribute getAttribute() { 41 | return pathHolder != null ? (SingularAttribute) pathHolder.getAttributes().get(0) : null; 42 | } 43 | 44 | public boolean isOrMode() { 45 | return orMode; 46 | } 47 | 48 | public void setOrMode(boolean orMode) { 49 | this.orMode = orMode; 50 | } 51 | 52 | public TermSelector or() { 53 | setOrMode(true); 54 | return this; 55 | } 56 | 57 | public TermSelector and() { 58 | setOrMode(false); 59 | return this; 60 | } 61 | 62 | /* 63 | * Get the possible candidates for property. 64 | */ 65 | public List getSelected() { 66 | return selected; 67 | } 68 | 69 | public void setSelected(String selected) { 70 | this.selected = newArrayList(selected); 71 | } 72 | 73 | /* 74 | * Set the possible candidates for property. 75 | */ 76 | public void setSelected(List selected) { 77 | this.selected = selected; 78 | } 79 | 80 | public TermSelector selected(String... selected) { 81 | setSelected(newArrayList(selected)); 82 | return this; 83 | } 84 | 85 | public boolean isNotEmpty() { 86 | if (selected == null || selected.isEmpty()) { 87 | return false; 88 | } 89 | for (String word : selected) { 90 | if (isNotBlank(word)) { 91 | return true; 92 | } 93 | } 94 | return false; 95 | } 96 | 97 | public void clearSelected() { 98 | if (selected != null) { 99 | selected.clear(); 100 | } 101 | } 102 | 103 | @Override 104 | public String toString() { 105 | StringBuilder s = new StringBuilder(); 106 | if (selected != null) { 107 | s.append("term"); 108 | if (selected.size() > 1) { 109 | s.append('s'); 110 | } 111 | s.append(": "); 112 | s.append(Arrays.toString(selected.toArray())); 113 | } 114 | if (pathHolder != null) { 115 | if (s.length() > 0) { 116 | s.append(' '); 117 | } 118 | s.append("on "); 119 | s.append(pathHolder.getPath()); 120 | } 121 | return s.toString(); 122 | } 123 | } -------------------------------------------------------------------------------- /src/test/java/demo/Account.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Source code generated by Celerio, a Jaxio product. 3 | * Documentation: http://www.jaxio.com/documentation/celerio/ 4 | * Follow us on twitter: @jaxiosoft 5 | * Need commercial support ? Contact us: info@jaxio.com 6 | * Template pack-backend-jpa:src/main/java/domain/Entity.e.vm.java 7 | * Template is part of Open Source Project: https://github.com/jaxio/pack-backend-jpa 8 | */ 9 | package demo; 10 | 11 | import com.google.common.base.MoreObjects; 12 | import com.google.common.base.Objects; 13 | import com.jaxio.jpa.querybyexample.Identifiable; 14 | import org.hibernate.annotations.GenericGenerator; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | import javax.persistence.*; 19 | import javax.persistence.criteria.CriteriaBuilder; 20 | import java.io.Serializable; 21 | import java.util.Date; 22 | 23 | import static javax.persistence.CascadeType.MERGE; 24 | import static javax.persistence.CascadeType.PERSIST; 25 | import static javax.persistence.FetchType.LAZY; 26 | import static javax.persistence.TemporalType.TIMESTAMP; 27 | 28 | @Entity 29 | @Table(name = "ACCOUNT") 30 | public class Account implements Identifiable, Serializable { 31 | private static final long serialVersionUID = 1L; 32 | private static final Logger log = LoggerFactory.getLogger(Account.class); 33 | 34 | // Raw attributes 35 | private Integer id; 36 | private String username; 37 | private String lastName; 38 | private Date birthDate; 39 | 40 | // Many to one 41 | private Address homeAddress; 42 | 43 | @Override 44 | @Column(name = "ID", precision = 10) 45 | @GeneratedValue 46 | @Id 47 | public Integer getId() { 48 | return id; 49 | } 50 | 51 | @Override 52 | public void setId(Integer id) { 53 | this.id = id; 54 | } 55 | 56 | @Override 57 | @Transient 58 | public boolean isIdSet() { 59 | return id != null; 60 | } 61 | 62 | // -- [username] ------------------------ 63 | 64 | @Column(name = "USERNAME", nullable = false, unique = true, length = 100) 65 | public String getUsername() { 66 | return username; 67 | } 68 | 69 | public void setUsername(String username) { 70 | this.username = username; 71 | } 72 | 73 | public Account username(String username) { 74 | setUsername(username); 75 | return this; 76 | } 77 | 78 | // -- [lastName] ------------------------ 79 | 80 | @Column(name = "LAST_NAME", length = 255) 81 | public String getLastName() { 82 | return lastName; 83 | } 84 | 85 | public void setLastName(String lastName) { 86 | this.lastName = lastName; 87 | } 88 | 89 | public Account lastName(String lastName) { 90 | setLastName(lastName); 91 | return this; 92 | } 93 | 94 | // -- [birthDate] ------------------------ 95 | 96 | @Column(name = "BIRTH_DATE", length = 23) 97 | @Temporal(TIMESTAMP) 98 | public Date getBirthDate() { 99 | return birthDate; 100 | } 101 | 102 | public void setBirthDate(Date birthDate) { 103 | this.birthDate = birthDate; 104 | } 105 | 106 | public Account birthDate(Date birthDate) { 107 | setBirthDate(birthDate); 108 | return this; 109 | } 110 | 111 | // ----------------------------------------------------------------- 112 | // Many to One support 113 | // ----------------------------------------------------------------- 114 | 115 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 116 | // many-to-one: Account.homeAddress ==> Address.id 117 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 118 | 119 | @JoinColumn(name = "ADDRESS_ID") 120 | @ManyToOne(cascade = { PERSIST, MERGE }, fetch = LAZY) 121 | public Address getHomeAddress() { 122 | return homeAddress; 123 | } 124 | 125 | /** 126 | * Set the {@link #homeAddress} without adding this Account instance on the passed {@link #homeAddress} 127 | */ 128 | public void setHomeAddress(Address homeAddress) { 129 | this.homeAddress = homeAddress; 130 | } 131 | 132 | public Account homeAddress(Address homeAddress) { 133 | setHomeAddress(homeAddress); 134 | return this; 135 | } 136 | 137 | /** 138 | * Equals implementation using a business key. 139 | */ 140 | @Override 141 | public boolean equals(Object other) { 142 | return this == other || (other instanceof Account && hashCode() == other.hashCode()); 143 | } 144 | 145 | private volatile int previousHashCode = 0; 146 | 147 | @Override 148 | public int hashCode() { 149 | int hashCode = Objects.hashCode(getUsername()); 150 | 151 | if (previousHashCode != 0 && previousHashCode != hashCode) { 152 | log.warn("DEVELOPER: hashCode has changed!." // 153 | + "If you encounter this message you should take the time to carefully review equals/hashCode for: " // 154 | + getClass().getCanonicalName()); 155 | } 156 | 157 | previousHashCode = hashCode; 158 | return hashCode; 159 | } 160 | 161 | /** 162 | * Construct a readable string representation for this Account instance. 163 | * 164 | * @see Object#toString() 165 | */ 166 | @Override 167 | public String toString() { 168 | return MoreObjects.toStringHelper(this) // 169 | .add("id", getId()) // 170 | .add("username", getUsername()) // 171 | .add("birthDate", getBirthDate()) // 172 | .toString(); 173 | } 174 | } -------------------------------------------------------------------------------- /src/test/java/demo/AccountQueryByExampleTest.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import com.jaxio.jpa.querybyexample.*; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.test.annotation.Rollback; 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import javax.inject.Inject; 13 | import java.text.DateFormat; 14 | import java.text.SimpleDateFormat; 15 | import java.util.Arrays; 16 | import java.util.Calendar; 17 | import java.util.Date; 18 | import java.util.List; 19 | 20 | import static org.hamcrest.CoreMatchers.is; 21 | 22 | @RunWith(SpringJUnit4ClassRunner.class) 23 | @ContextConfiguration(locations = {"classpath*:applicationContext.xml"}) 24 | @Transactional 25 | public class AccountQueryByExampleTest { 26 | @Inject 27 | AccountRepository accountRepository; 28 | 29 | @Test 30 | @Rollback 31 | public void simpleQueryOnLastName() throws Exception { 32 | Account example = new Account().lastName("Jagger"); 33 | //SearchParameters sp = new SearchParameters().caseInsensitive(); 34 | List result = accountRepository.find(example); 35 | Assert.assertThat(result.get(0).getUsername(), is("mick")); 36 | } 37 | 38 | @Test 39 | @Rollback 40 | public void simpleQueryOnLastNameCaseInsensitiveWithOrderBy() throws Exception { 41 | Account example = new Account().lastName("Jagger"); 42 | example.setLastName("Jagger"); 43 | SearchParameters sp = new SearchParameters().caseInsensitive().orderBy(OrderByDirection.ASC, Account_.lastName); 44 | List result = accountRepository.find(example, sp); 45 | Assert.assertThat(result.get(0).getUsername(), is("mick")); 46 | } 47 | 48 | @Test 49 | @Rollback 50 | public void simpleQueryOnLastNameWithOrderByAndPagination() throws Exception { 51 | Account example = new Account(); 52 | example.setLastName("Jagger"); 53 | SearchParameters sp = new SearchParameters().orderBy(OrderByDirection.ASC, Account_.lastName) // 54 | .first(50).maxResults(25); 55 | List result = accountRepository.find(example, sp); 56 | Assert.assertThat(result.size(), is(0)); 57 | } 58 | 59 | @Test 60 | @Rollback 61 | public void simpleQueryOnLastNameWithLike() throws Exception { 62 | Account example = new Account(); 63 | example.setLastName("Jag"); 64 | SearchParameters sp = new SearchParameters().startingLike(); 65 | List result = accountRepository.find(example, sp); 66 | Assert.assertThat(result.size(), is(1)); 67 | } 68 | 69 | @Test 70 | @Rollback 71 | public void simpleQueryOnSeveralFields_AND() throws Exception { 72 | Account example = new Account(); 73 | example.setLastName("Jag"); 74 | example.setBirthDate(new Date()); 75 | SearchParameters sp = new SearchParameters().orderBy(OrderByDirection.ASC, Account_.lastName).startingLike(); 76 | List result = accountRepository.find(example, sp); 77 | Assert.assertThat(result.size(), is(0)); 78 | } 79 | 80 | @Test 81 | @Rollback 82 | public void simpleQueryOnSeveralFields_OR() throws Exception { 83 | Account example = new Account(); 84 | example.setLastName("Jag"); 85 | example.setBirthDate(new Date()); 86 | SearchParameters sp = new SearchParameters().orMode().orderBy(OrderByDirection.ASC, Account_.lastName).startingLike(); 87 | List result = accountRepository.find(example, sp); 88 | Assert.assertThat(result.size(), is(1)); 89 | } 90 | 91 | @Test 92 | @Rollback 93 | public void dateRangeQuery() throws Exception { 94 | Account example = new Account(); 95 | example.setLastName("Jagger"); 96 | 97 | Calendar from = Calendar.getInstance(); 98 | from.set(1940, 0, 1); 99 | 100 | Calendar to = Calendar.getInstance(); 101 | to.set(1945, 11, 31); 102 | 103 | Range birthDateRange = Range.newRange(Account_.birthDate); 104 | birthDateRange.from(from.getTime()).to(to.getTime()); 105 | 106 | SearchParameters sp = new SearchParameters().range(birthDateRange); 107 | List result = accountRepository.find(example, sp); 108 | 109 | Assert.assertThat(result.size(), is(1)); 110 | Assert.assertThat(result.get(0).getUsername(), is("mick")); 111 | System.out.println("******************************************"); 112 | System.out.println(result.get(0)); 113 | System.out.println("******************************************"); 114 | } 115 | 116 | @Test 117 | @Rollback 118 | public void dateRangeQueryVariation() throws Exception { 119 | DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); 120 | Date from = dateFormat.parse("1920-12-01"); 121 | Date to = dateFormat.parse("1974-12-01"); 122 | 123 | SearchParameters sp = new SearchParameters().range(from, to, Account_.birthDate); 124 | List accountList = accountRepository.find(sp); 125 | Assert.assertThat(accountList.size(), is(4)); 126 | } 127 | 128 | @Test 129 | @Rollback 130 | public void dateRangeAndLikeQuery() throws Exception { 131 | DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); 132 | Date from = dateFormat.parse("1920-12-01"); 133 | Date to = dateFormat.parse("2020-12-01"); 134 | 135 | SearchParameters sp = new SearchParameters().range(from, to, Account_.birthDate); 136 | sp.anywhere(); // enable search like '%...%' 137 | 138 | Account example = new Account(); 139 | example.setUsername("i"); // will search username Like '%i%' 140 | 141 | List accountList = accountRepository.find(example, sp); 142 | System.out.println("******************************************"); 143 | for (Account a : accountList) { 144 | System.out.println(a); 145 | } 146 | System.out.println("******************************************"); 147 | } 148 | 149 | 150 | @Test 151 | @Rollback 152 | public void matchAllStringProperty() throws Exception { 153 | SearchParameters sp = new SearchParameters().searchMode(SearchMode.STARTING_LIKE).searchPattern("Jag"); 154 | List result = accountRepository.find(sp); 155 | Assert.assertThat(result.size(), is(1)); 156 | Assert.assertThat(result.get(0).getUsername(), is("mick")); 157 | } 158 | 159 | @Test 160 | @Rollback 161 | public void propertySelectorOnLastName() throws Exception { 162 | PropertySelector lastNameSelector = PropertySelector.newPropertySelector(Account_.lastName); 163 | lastNameSelector.setSelected(Arrays.asList("Jagger", "Richards", "Jones", "Watts", "taylor", "Wyman", "Wood")); 164 | 165 | SearchParameters sp = new SearchParameters().property(lastNameSelector); 166 | 167 | List result = accountRepository.find(sp); 168 | Assert.assertThat(result.size(), is(3)); 169 | } 170 | 171 | @Test 172 | @Rollback 173 | public void queryInvolvingManyToOne() throws Exception { 174 | Account example = new Account(); 175 | example.setHomeAddress(new Address()); 176 | example.getHomeAddress().setCity("Paris"); 177 | List result = accountRepository.find(example); 178 | Assert.assertThat(result.size(), is(2)); 179 | } 180 | } -------------------------------------------------------------------------------- /src/test/java/demo/AccountRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Source code generated by Celerio, a Jaxio product. 3 | * Documentation: http://www.jaxio.com/documentation/celerio/ 4 | * Follow us on twitter: @jaxiosoft 5 | * Need commercial support ? Contact us: info@jaxio.com 6 | * Template pack-backend-jpa:src/main/java/repository/Repository.e.vm.java 7 | * Template is part of Open Source Project: https://github.com/jaxio/pack-backend-jpa 8 | */ 9 | package demo; 10 | 11 | import com.jaxio.jpa.querybyexample.GenericRepository; 12 | 13 | import javax.inject.Named; 14 | import javax.inject.Singleton; 15 | 16 | @Named 17 | @Singleton 18 | public class AccountRepository extends GenericRepository { 19 | 20 | public AccountRepository() { 21 | super(Account.class); 22 | } 23 | 24 | @Override 25 | public Account getNew() { 26 | return new Account(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/java/demo/Account_.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Source code generated by Celerio, a Jaxio product. 3 | * Documentation: http://www.jaxio.com/documentation/celerio/ 4 | * Follow us on twitter: @jaxiosoft 5 | * Need commercial support ? Contact us: info@jaxio.com 6 | * Template pack-backend-jpa:src/main/java/domain/EntityMeta_.e.vm.java 7 | * Template is part of Open Source Project: https://github.com/jaxio/pack-backend-jpa 8 | */ 9 | package demo; 10 | 11 | import javax.persistence.metamodel.SingularAttribute; 12 | import javax.persistence.metamodel.StaticMetamodel; 13 | import java.util.Date; 14 | 15 | @StaticMetamodel(Account.class) 16 | public abstract class Account_ { 17 | 18 | // Raw attributes 19 | public static volatile SingularAttribute id; 20 | public static volatile SingularAttribute username; 21 | public static volatile SingularAttribute lastName; 22 | public static volatile SingularAttribute birthDate; 23 | 24 | // Many to one 25 | public static volatile SingularAttribute homeAddress; 26 | } -------------------------------------------------------------------------------- /src/test/java/demo/Address.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Source code generated by Celerio, a Jaxio product. 3 | * Documentation: http://www.jaxio.com/documentation/celerio/ 4 | * Follow us on twitter: @jaxiosoft 5 | * Need commercial support ? Contact us: info@jaxio.com 6 | * Template pack-backend-jpa:src/main/java/domain/Entity.e.vm.java 7 | * Template is part of Open Source Project: https://github.com/jaxio/pack-backend-jpa 8 | */ 9 | package demo; 10 | 11 | import com.google.common.base.MoreObjects; 12 | import com.jaxio.jpa.querybyexample.Identifiable; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import javax.persistence.*; 17 | import java.io.Serializable; 18 | 19 | @Entity 20 | @Table(name = "ADDRESS") 21 | public class Address implements Identifiable, Serializable { 22 | private static final long serialVersionUID = 1L; 23 | private static final Logger log = LoggerFactory.getLogger(Address.class); 24 | 25 | // Raw attributes 26 | private Integer id; 27 | private String streetName; 28 | private String city; 29 | 30 | // -- [id] ------------------------ 31 | 32 | @Override 33 | @Column(name = "ID", precision = 10) 34 | @GeneratedValue 35 | @Id 36 | public Integer getId() { 37 | return id; 38 | } 39 | 40 | @Override 41 | public void setId(Integer id) { 42 | this.id = id; 43 | } 44 | 45 | @Override 46 | @Transient 47 | public boolean isIdSet() { 48 | return id != null; 49 | } 50 | 51 | // -- [streetName] ------------------------ 52 | 53 | @Column(name = "STREET_NAME", length = 100) 54 | public String getStreetName() { 55 | return streetName; 56 | } 57 | 58 | public void setStreetName(String streetName) { 59 | this.streetName = streetName; 60 | } 61 | 62 | public Address streetName(String streetName) { 63 | setStreetName(streetName); 64 | return this; 65 | } 66 | 67 | // -- [city] ------------------------ 68 | 69 | @Column(name = "CITY", nullable = false, length = 100) 70 | public String getCity() { 71 | return city; 72 | } 73 | 74 | public void setCity(String city) { 75 | this.city = city; 76 | } 77 | 78 | public Address city(String city) { 79 | setCity(city); 80 | return this; 81 | } 82 | 83 | /** 84 | * Equals implementation using a business key. 85 | */ 86 | @Override 87 | public boolean equals(Object other) { 88 | return this == other || (other instanceof Address && hashCode() == other.hashCode()); 89 | } 90 | 91 | private IdentifiableHashBuilder identifiableHashBuilder = new IdentifiableHashBuilder(); 92 | 93 | @Override 94 | public int hashCode() { 95 | return identifiableHashBuilder.hash(log, this); 96 | } 97 | 98 | /** 99 | * Construct a readable string representation for this Address instance. 100 | * 101 | * @see Object#toString() 102 | */ 103 | @Override 104 | public String toString() { 105 | return MoreObjects.toStringHelper(this) // 106 | .add("id", getId()) // 107 | .add("streetName", getStreetName()) // 108 | .add("city", getCity()) // 109 | .toString(); 110 | } 111 | } -------------------------------------------------------------------------------- /src/test/java/demo/Address_.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Source code generated by Celerio, a Jaxio product. 3 | * Documentation: http://www.jaxio.com/documentation/celerio/ 4 | * Follow us on twitter: @jaxiosoft 5 | * Need commercial support ? Contact us: info@jaxio.com 6 | * Template pack-backend-jpa:src/main/java/domain/EntityMeta_.e.vm.java 7 | * Template is part of Open Source Project: https://github.com/jaxio/pack-backend-jpa 8 | */ 9 | package demo; 10 | 11 | import javax.persistence.metamodel.SingularAttribute; 12 | import javax.persistence.metamodel.StaticMetamodel; 13 | 14 | @StaticMetamodel(Address.class) 15 | public abstract class Address_ { 16 | 17 | // Raw attributes 18 | public static volatile SingularAttribute id; 19 | public static volatile SingularAttribute streetName; 20 | public static volatile SingularAttribute city; 21 | public static volatile SingularAttribute version; 22 | } -------------------------------------------------------------------------------- /src/test/java/demo/IdentifiableHashBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Source code generated by Celerio, a Jaxio product. 3 | * Documentation: http://www.jaxio.com/documentation/celerio/ 4 | * Follow us on twitter: @jaxiosoft 5 | * Need commercial support ? Contact us: info@jaxio.com 6 | * Template pack-backend-jpa:src/main/java/domain/support/IdentifiableHashBuilder.p.vm.java 7 | * Template is part of Open Source Project: https://github.com/jaxio/pack-backend-jpa 8 | */ 9 | package demo; 10 | 11 | import com.jaxio.jpa.querybyexample.Identifiable; 12 | import org.slf4j.Logger; 13 | 14 | import java.io.Serializable; 15 | 16 | /** 17 | * The first time the {@link #hash(Logger, Identifiable)} is called, we check if the primary key is present or not. 18 | *

    19 | *
  • If yes: we use it to get the hash
  • 20 | *
  • If no: we use a VMID during the entire life of this instance even if later on this instance is assigned a primary key.
  • 21 | *
22 | */ 23 | public class IdentifiableHashBuilder implements Serializable { 24 | private static final long serialVersionUID = 1L; 25 | private Object technicalId; 26 | 27 | public int hash(Logger log, Identifiable identifiable) { 28 | if (technicalId == null) { 29 | if (identifiable.isIdSet()) { 30 | technicalId = identifiable.getId(); 31 | } else { 32 | technicalId = new java.rmi.dgc.VMID(); 33 | log.warn("DEVELOPER: hashCode is not safe." // 34 | + "If you encounter this message you should take the time to carefully " // 35 | + "review the equals/hashCode methods for: " + identifiable.getClass().getCanonicalName() // 36 | + " You may consider using a business key."); 37 | } 38 | } 39 | return technicalId.hashCode(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/demo/JpaConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Source code generated by Celerio, a Jaxio product. 3 | * Documentation: http://www.jaxio.com/documentation/celerio/ 4 | * Follow us on twitter: @jaxiosoft 5 | * Need commercial support ? Contact us: info@jaxio.com 6 | * Template pack-backend-jpa:src/main/java/configuration/JpaConfiguration.p.vm.java 7 | * Template is part of Open Source Project: https://github.com/jaxio/pack-backend-jpa 8 | */ 9 | package demo; 10 | 11 | import org.hibernate.SessionFactory; 12 | import org.hibernate.jpa.HibernateEntityManagerFactory; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; 17 | import org.springframework.orm.jpa.JpaTransactionManager; 18 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 19 | import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; 20 | 21 | import javax.annotation.Resource; 22 | import javax.sql.DataSource; 23 | import java.util.Properties; 24 | 25 | @Configuration 26 | public class JpaConfiguration { 27 | 28 | @Resource(name = "dataSource") 29 | private DataSource dataSource; 30 | 31 | /** 32 | * Enable exception translation for beans annotated with @Repository 33 | */ 34 | @Bean 35 | public PersistenceExceptionTranslationPostProcessor exceptionTranslation() { 36 | return new PersistenceExceptionTranslationPostProcessor(); 37 | } 38 | 39 | /** 40 | * @see http://www.springframework.org/docs/reference/transaction.html 41 | */ 42 | @Bean 43 | public JpaTransactionManager transactionManager() { 44 | return new JpaTransactionManager(); 45 | } 46 | 47 | /** 48 | * Build the entity manager with Hibernate as a provider. 49 | */ 50 | @Bean 51 | public LocalContainerEntityManagerFactoryBean entityManagerFactory() { 52 | LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean(); 53 | emf.setDataSource(dataSource); 54 | // We set the persistenceXmlLocation to a different name to make it work on JBoss. 55 | emf.setPersistenceXmlLocation("classpath:META-INF/persistence.xml"); 56 | emf.setPersistenceUnitName("demoPU"); 57 | emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); 58 | return emf; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/resources/01-create.sql: -------------------------------------------------------------------------------- 1 | DROP ALL OBJECTS; 2 | 3 | CREATE TABLE ADDRESS ( 4 | id int not null IDENTITY, 5 | street_name varchar(100), 6 | city varchar(100) not null, 7 | version int default 0, 8 | 9 | primary key (id) 10 | ); 11 | 12 | CREATE TABLE ACCOUNT ( 13 | id int not null IDENTITY, 14 | username varchar(100) not null, 15 | last_name varchar(255), 16 | birth_date timestamp, 17 | address_id int, 18 | constraint account_unique_1 unique (username), 19 | constraint account_fk_1 foreign key (address_id) references ADDRESS, 20 | primary key (id) 21 | ); 22 | 23 | -- use negative id to avoid conflict with auto generated ids. 24 | INSERT INTO ADDRESS (id, street_name, city) values(-1, 'Avenue des champs Elysées', 'Paris'); 25 | INSERT INTO ADDRESS (id, street_name, city) values(-2, 'Park avenue', 'New-York'); 26 | INSERT INTO ADDRESS (id, street_name, city) values(-3, 'Tochomae', 'Tokyo'); 27 | INSERT INTO ADDRESS (id, street_name, city) values(-4, 'California Street', 'San Francisco'); 28 | 29 | -- use negative id to avoid conflict with auto generated ids. 30 | INSERT INTO ACCOUNT (id, username, last_name, birth_date, address_id) VALUES (-1, 'nico', 'Romanetti', {d '1956-12-08'}, -1); 31 | INSERT INTO ACCOUNT (id, username, last_name, birth_date, address_id) VALUES (-2, 'flo', 'Ramimère', {d '1975-08-18'}, -2); 32 | INSERT INTO ACCOUNT (id, username, last_name, birth_date, address_id) VALUES (-3, 'bibi', 'Sock', {d '1979-01-08'}, -3); 33 | INSERT INTO ACCOUNT (id, username, last_name, birth_date, address_id) VALUES (-4, 'jlb', 'Boudart', {d '2012-10-30'}, -4); 34 | INSERT INTO ACCOUNT (id, username, last_name, birth_date, address_id) VALUES (-5, 'mick', 'Jagger', {d '1943-10-30'}, -1); 35 | INSERT INTO ACCOUNT (id, username, last_name, birth_date, address_id) VALUES (-6, 'keith', 'Richards', {d '1943-10-30'}, -2); 36 | INSERT INTO ACCOUNT (id, username, last_name, birth_date, address_id) VALUES (-7, 'charlie', 'Watts', {d '1941-10-30'}, -3); 37 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/persistence.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | demo.Account 13 | 14 | -------------------------------------------------------------------------------- /src/test/resources/applicationContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/test/resources/hibernate.properties: -------------------------------------------------------------------------------- 1 | #----------------------- 2 | # HIBERNATE PROPERTIES 3 | #----------------------- 4 | 5 | hibernate.dialect=org.hibernate.dialect.H2Dialect 6 | hibernate.connection.useUnicode=true 7 | hibernate.connection.charSet=UTF-8 8 | 9 | # logging debug information 10 | hibernate.show_sql=true 11 | hibernate.format_sql=true 12 | hibernate.use_sql_comments=false 13 | --------------------------------------------------------------------------------