├── .gitignore ├── .idea ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img ├── eclipse-annotation-factorypath.png ├── eclipse-annotation-processors.png ├── eclipse-errors.png └── intellij-annotation-processors.png ├── query-validator.iml ├── settings.gradle └── src ├── main ├── java │ └── org │ │ └── hibernate │ │ └── query │ │ └── validator │ │ ├── ECJASTVisitor.java │ │ ├── ECJErrorReporter.java │ │ ├── ECJProcessor.java │ │ ├── ECJSessionFactory.java │ │ ├── EclipseChecker.groovy │ │ ├── EclipseErrorReporter.groovy │ │ ├── EclipseProcessor.groovy │ │ ├── EclipseSessionFactory.groovy │ │ ├── HQLProcessor.java │ │ ├── JavacChecker.java │ │ ├── JavacErrorReporter.java │ │ ├── JavacProcessor.java │ │ ├── JavacSessionFactory.java │ │ ├── JavacTreeScanner.java │ │ ├── MockCollectionPersister.java │ │ ├── MockEntityPersister.java │ │ ├── MockJdbcServicesInitiator.java │ │ ├── MockSessionFactory.java │ │ ├── Mocker.java │ │ ├── ModularityWorkaround.java │ │ ├── PanacheUtils.java │ │ ├── Parent.java │ │ ├── Permit.java │ │ ├── ProcessorSessionFactory.java │ │ └── Validation.java └── resources │ └── META-INF │ └── services │ └── javax.annotation.processing.Processor └── test ├── java └── org │ └── hibernate │ └── query │ └── validator │ └── test │ └── HQLValidationTest.java └── source └── test ├── Address.java ├── BadQueries.java ├── Country.java ├── Email.java ├── Employee.java ├── GoodQueries.java ├── Pair.java ├── PanacheBadPerson.java ├── PanacheBadPersonRepository.java ├── PanachePerson.java ├── PanachePersonRepository.java ├── Person.java ├── Sex.java ├── package-info.java └── test ├── Rating.java └── package-info.java /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | .idea/workspace.xml 3 | out/ 4 | target/ 5 | build/ 6 | .gradle 7 | dependency-reduced-pom.xml 8 | test-runtime-libs/ 9 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | ![Hibernate logo][] 2 | 3 | # Hibernate Query Validator 4 | 5 | Compile time validation for queries written in HQL, JPQL, and 6 | [Panache][]. 7 | 8 | [Panache]: https://quarkus.io/guides/hibernate-orm-panache 9 | [Hibernate logo]: http://static.jboss.org/hibernate/images/hibernate_logo_whitebkg_200px.png 10 | 11 | ## Requirements 12 | 13 | This project now requires at least JDK 11, but JDK 15 or above 14 | is preferred. 15 | 16 | ## Building 17 | 18 | Type `./gradlew` from this project directory. 19 | 20 | This produces an artifact with the Maven coordinates 21 | `org.hibernate:query-validator:2.0-SNAPSHOT` in your local 22 | Maven repository. 23 | 24 | It also creates a far jar `query-validator-2.0-SNAPSHOT-all.jar` 25 | in the `build/libs` directory of this project. 26 | 27 | ## Usage 28 | 29 | The persistent entity classes *must* be annotated with the 30 | basic JPA metadata annotations like `@Entity`, `@ManyToOne`, 31 | `@Embeddable`, `@MappedSuperclass`, `@ElementCollection`, and 32 | `@Access`. You *may* use XML-based mappings to specify database 33 | mapping information like table and column names if that's what 34 | you prefer. But entities mapped *completely* in XML will not be 35 | discovered by the query validator. 36 | 37 | 1. Put `query-validator-2.0-SNAPSHOT-all.jar` in the 38 | compile-time classpath of your project. (Or depend on 39 | `org.hibernate:query-validator:2.0-SNAPSHOT`.) 40 | 2. Annotate a package or toplevel class with `@CheckHQL`. 41 | 42 | #### Usage with plain Hibernate or JPA 43 | 44 | The validator will check any static string argument of 45 | 46 | - the `createQuery()`, `createSelectionQuery()`, and 47 | `createMutationQuery()` methods, 48 | - the `@NamedQuery()` annotation, or 49 | - the `@HQL` annotation 50 | 51 | which occurs in a package, class, or interface annotated 52 | `@CheckHQL`. 53 | 54 | #### Usage with Panache 55 | 56 | Inside a Panache entity or repository, the following queries 57 | will be checked: 58 | 59 | - `list()`, `find()`, and `stream()`, 60 | - `count()`, 61 | - `delete()`, and 62 | - `update()` 63 | 64 | ### Errors and warnings 65 | 66 | The purpose of the query validator is to detect erroneous 67 | query strings and query parameter bindings when the Java code 68 | is compiled, instead of at runtime when the query is executed. 69 | 70 | #### Errors 71 | 72 | A compile-time error is produced if: 73 | 74 | - the query has syntax errors, 75 | - an entity name in the query doesn't reference a persistent 76 | entity class, 77 | - a member name in the query doesn't reference a mapped field 78 | or property of the entity, or 79 | - there is some other typing error, for example, incorrect 80 | function argument types. 81 | 82 | #### Warnings 83 | 84 | Additionally, any JPA `Query` instance that is created and 85 | immediately invoked in a single expression will have its 86 | parameter bindings validated. A warning is produced if: 87 | 88 | - the query string has a parameter with no argument specified 89 | using `setParameter()`, or 90 | - an argument is specified using `setParameter()`, but there 91 | is no matching parameter in the query string. 92 | 93 | All Panache queries have their parameters validated. 94 | 95 | ### Usage from command line 96 | 97 | When using a command line compiler, `gradle`, or `mvn`, errors 98 | from the query validator are displayed in the compiler output 99 | alongside other compilation errors. 100 | 101 | #### `javac` and ECJ 102 | 103 | Just compile your code with `javac`, or even with ECJ 104 | (`java -jar ecj-4.6.1.jar`), with the query validator `jar` in 105 | the classpath: 106 | 107 | -classpath query-validator-2.0-SNAPSHOT-all.jar 108 | 109 | Of course, you'll also need Hibernate core on the classpath. 110 | 111 | #### Gradle 112 | 113 | In principle, it's enough to declare dependencies on Hibernate core 114 | and on the query validator, just like this: 115 | 116 | dependencies { 117 | implementation 'org.hibernate.orm:hibernate-core:6.3.0-SNAPSHOT' 118 | annotationProcessor 'org.hibernate:query-validator:2.0-SNAPSHOT' 119 | } 120 | 121 | Unfortunately, this often results in some quite annoying warnings 122 | from `javac`. Get rid of them by also declaring an `implementation` 123 | dependency on the Query validator: 124 | 125 | dependencies { 126 | implementation 'org.hibernate:query-validator:2.0-SNAPSHOT' 127 | annotationProcessor 'org.hibernate:query-validator:2.0-SNAPSHOT' 128 | implementation 'org.hibernate:query-validator:2.0-SNAPSHOT' 129 | } 130 | 131 | #### Maven 132 | 133 | Maven handles annotation processors correctly. Just declare the 134 | dependency on the query validator: 135 | 136 | 137 | 138 | org.hibernate 139 | query-validator 140 | 2.0-SNAPSHOT 141 | true 142 | 143 | 144 | 145 | ### Usage in IDEs 146 | 147 | Both IntelliJ and Eclipse require that annotation processing 148 | be explicitly enabled. 149 | 150 | #### IntelliJ 151 | 152 | Select **Enable annotation processing** in IntelliJ IDEA 153 | preferences under **Build, Execution, Deployment > Compiler > 154 | AnnotationProcessors**. 155 | 156 | ![IntelliJ Screenshot 1](img/intellij-annotation-processors.png) 157 | 158 | You do not need to do this if you're using Gradle to build 159 | your project. 160 | 161 | IntelliJ only runs annotation processors during a build (that 162 | is, when you `Run` your code or explicitly `Build Project`). 163 | So you won't see errors in your Java editor as you're typing. 164 | 165 | #### Eclipse 166 | 167 | Eclipse IDE doesn't load annotation processors from the 168 | project classpath. So you'll need to add the query validator 169 | manually. 170 | 171 | 1. In **Project > Properties** go to **Java Compiler > 172 | Annotation Processing** and select **Enable annotation 173 | processing**. 174 | 2. Then go to **Java Compiler > Annotation Processing > 175 | Factory Path** and click **Add External JARs...** and 176 | add `build/libs/query-validator-2.0-SNAPSHOT-all.jar` 177 | from this project directory. 178 | 179 | Your project properties should look like this: 180 | 181 | ![Eclipse Screenshot 1](img/eclipse-annotation-processors.png) 182 | ![Eclipse Screenshot 2](img/eclipse-annotation-factorypath.png) 183 | 184 | Eclipse runs annotation processors during every incremental 185 | build (that is, every time you `Save`), so you'll see errors 186 | displayed inline in your Java editor. 187 | 188 | ![Eclipse Screenshot 3](img/eclipse-errors.png) 189 | 190 | If the query validator doesn't run, please ensure that: 191 | 192 | - Eclipse itself is running on a compatible JDK. 193 | - Your project is set up to compile with a compatible Java 194 | compiler, and the compiler compliance level is set to at 195 | least 1.8. 196 | 197 | ## Compatibility 198 | 199 | The query validator was developed and tested with: 200 | 201 | - JDK 15, JDK 17, JDK 20 202 | - Hibernate 6.3.0 203 | - ECJ 3.33.0 204 | - Eclipse IDE with JDT Core 3.33.0 205 | 206 | Other versions of `javac`, ECJ, and Hibernate may or may not 207 | work. The query validator depends on internal compiler APIs in 208 | `javac` and ECJ, and is therefore sensitive to changes in the 209 | compilers. 210 | 211 | ## Caveats 212 | 213 | Please be aware of the following issues. 214 | 215 | #### HQL is a superset of JPQL 216 | 217 | Queries are interpreted according to Hibernate's flavor of JPQL 218 | (i.e. HQL), which is a superset of the query language defined by 219 | the JPA specification. Queries accepted by the query validator 220 | may not execute correctly on other implementations of JPA. 221 | 222 | #### Explicit entity names are not supported in Eclipse/ECJ 223 | 224 | In ECJ, don't use `@Entity(name="Whatever")`, since, during an 225 | incremental build, the processor won't be able to discover the 226 | entity named `Whatever`. Just let the entity name default to 227 | the name of the class. 228 | 229 | #### Ugly error messages 230 | 231 | Please report ugly, confusing, or badly-formatted error messages 232 | as bugs. 233 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'groovy' 4 | id 'maven-publish' 5 | id "com.github.johnrengelman.shadow" version "8.1.1" 6 | } 7 | 8 | defaultTasks 'assemble', 'publishToMavenLocal', 'shadowJar', 'test' 9 | 10 | repositories { 11 | mavenLocal() 12 | maven { 13 | url = 'https://repo.maven.apache.org/maven2' 14 | } 15 | } 16 | 17 | dependencies { 18 | implementation ('org.hibernate.orm:hibernate-core:6.3.0.Final') { 19 | transitive = false 20 | } 21 | //explicit the Hibernate dependencies we need: 22 | implementation 'org.antlr:antlr4-runtime:4.10.1' 23 | implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' 24 | runtimeOnly 'jakarta.transaction:jakarta.transaction-api:2.0.1' 25 | implementation 'net.bytebuddy:byte-buddy:1.14.5' 26 | runtimeOnly 'org.jboss.logging:jboss-logging:3.5.0.Final' 27 | runtimeOnly 'com.google.code.findbugs:jsr305:3.0.2' 28 | implementation 'org.hibernate.common:hibernate-commons-annotations:6.0.6.Final' 29 | implementation 'io.smallrye:jandex:3.1.1' 30 | runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' 31 | 32 | testRuntimeOnly ('io.quarkus:quarkus-hibernate-orm-panache:3.1.0.Final') { 33 | transitive = false 34 | } 35 | testRuntimeOnly ('io.quarkus:quarkus-panache-common:3.1.0.Final') { 36 | transitive = false 37 | } 38 | 39 | implementation 'org.apache.groovy:groovy:4.0.12' 40 | 41 | implementation 'org.eclipse.jdt:ecj:3.33.0' 42 | 43 | // testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.3' 44 | testImplementation 'junit:junit:4.13.2' 45 | 46 | implementation 'javax.xml.bind:jaxb-api:2.3.1' 47 | } 48 | 49 | group = 'org.hibernate' 50 | version = '2.0-SNAPSHOT' 51 | description = 'query-validator' 52 | sourceCompatibility = '8' 53 | 54 | sourceSets { 55 | main { 56 | groovy { 57 | srcDirs = ['src/main/java'] 58 | } 59 | } 60 | } 61 | 62 | shadowJar { 63 | dependencies { 64 | exclude(dependency('org.eclipse.jdt:ecj')) 65 | // exclude "tools.jar" 66 | } 67 | relocate ('org.hibernate', 'org.hibernate.query.validator.orm') { 68 | exclude 'org.hibernate.query.validator.*' 69 | } 70 | relocate ('org.jboss', 'org.hibernate.query.validator') { 71 | exclude 'org.jboss.logging.*' 72 | } 73 | relocate 'jakarta.persistence', 'org.hibernate.query.validator.jakarta.jpa' 74 | relocate 'jakarta.transaction', 'org.hibernate.query.validator.jakarta.jta' 75 | exclude 'jakarta.activation' 76 | relocate 'net', 'org.hibernate.query.validator' 77 | relocate 'org.antlr.v4.runtime', 'org.hibernate.query.validator.antlr' 78 | relocate 'org.jboss.jandex', 'org.hibernate.query.validator.jandex' 79 | relocate 'org.apache.groovy', 'org.hibernate.query.validator.groovy.apache' 80 | relocate ('org.codehaus.groovy', 'org.hibernate.query.validator.groovy.codehaus') { 81 | exclude 'org.codehaus.groovy.runtime.*' 82 | exclude 'org.codehaus.groovy.runtime.callsite.*' 83 | } 84 | relocate ('groovy', 'org.hibernate.query.validator.groovy.groovy') { 85 | exclude 'groovy.lang.*' 86 | } 87 | relocate 'groovyjarjarantlr', 'org.hibernate.query.validator.groovy.antlr' 88 | relocate 'groovyjarjarasm', 'org.hibernate.query.validator.asm' 89 | relocate 'groovyjarjarcommonscli', 'org.hibernate.query.validator.cli' 90 | relocate 'groovyjarjarpicocli', 'org.hibernate.query.validator.picocli' 91 | } 92 | 93 | jar { 94 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 95 | } 96 | 97 | publishing { 98 | publications { 99 | shadow(MavenPublication) { publication -> 100 | project.shadow.component(publication) 101 | } 102 | } 103 | } 104 | 105 | test { 106 | dependsOn 'copyDependencies' 107 | systemProperty 'gradle', 'true' 108 | } 109 | 110 | tasks.withType(JavaCompile) { 111 | options.encoding = 'UTF-8' 112 | } 113 | 114 | task copyDependencies(type: Copy) { 115 | from configurations.testRuntimeClasspath 116 | into 'test-runtime-libs' 117 | } 118 | 119 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hibernate/query-validator/bd1d1585535223e4e99bdc773e5a7da49309277e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /img/eclipse-annotation-factorypath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hibernate/query-validator/bd1d1585535223e4e99bdc773e5a7da49309277e/img/eclipse-annotation-factorypath.png -------------------------------------------------------------------------------- /img/eclipse-annotation-processors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hibernate/query-validator/bd1d1585535223e4e99bdc773e5a7da49309277e/img/eclipse-annotation-processors.png -------------------------------------------------------------------------------- /img/eclipse-errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hibernate/query-validator/bd1d1585535223e4e99bdc773e5a7da49309277e/img/eclipse-errors.png -------------------------------------------------------------------------------- /img/intellij-annotation-processors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hibernate/query-validator/bd1d1585535223e4e99bdc773e5a7da49309277e/img/intellij-annotation-processors.png -------------------------------------------------------------------------------- /query-validator.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'query-validator' 2 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/ECJASTVisitor.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import org.eclipse.jdt.internal.compiler.ASTVisitor; 4 | import org.eclipse.jdt.internal.compiler.Compiler; 5 | import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; 6 | import org.eclipse.jdt.internal.compiler.ast.Expression; 7 | import org.eclipse.jdt.internal.compiler.ast.IntLiteral; 8 | import org.eclipse.jdt.internal.compiler.ast.MemberValuePair; 9 | import org.eclipse.jdt.internal.compiler.ast.MessageSend; 10 | import org.eclipse.jdt.internal.compiler.ast.SingleNameReference; 11 | import org.eclipse.jdt.internal.compiler.ast.StringLiteral; 12 | import org.eclipse.jdt.internal.compiler.ast.ThisReference; 13 | import org.eclipse.jdt.internal.compiler.lookup.BlockScope; 14 | 15 | import javax.annotation.processing.ProcessingEnvironment; 16 | import javax.lang.model.element.TypeElement; 17 | import java.util.HashSet; 18 | import java.util.Set; 19 | 20 | import static java.lang.Integer.parseInt; 21 | import static org.eclipse.jdt.core.compiler.CharOperation.charToString; 22 | import static org.hibernate.query.validator.ECJSessionFactory.qualifiedName; 23 | import static org.hibernate.query.validator.HQLProcessor.hibernate; 24 | import static org.hibernate.query.validator.HQLProcessor.jpa; 25 | import static org.hibernate.query.validator.Validation.validate; 26 | 27 | /** 28 | * @author Gavin King 29 | */ 30 | class ECJASTVisitor extends ASTVisitor { 31 | final Set setParameterLabels; 32 | final Set setParameterNames; 33 | final Set setOrderBy; 34 | private final TypeElement panacheEntity; 35 | private final CompilationUnitDeclaration unit; 36 | private final Compiler compiler; 37 | private final ProcessingEnvironment processingEnv; 38 | boolean immediatelyCalled; 39 | 40 | public ECJASTVisitor(TypeElement panacheEntity, CompilationUnitDeclaration unit, Compiler compiler, ProcessingEnvironment processingEnv) { 41 | this.panacheEntity = panacheEntity; 42 | this.unit = unit; 43 | this.compiler = compiler; 44 | this.processingEnv = processingEnv; 45 | setParameterLabels = new HashSet<>(); 46 | setParameterNames = new HashSet<>(); 47 | setOrderBy = new HashSet<>(); 48 | } 49 | 50 | @Override 51 | public boolean visit(MessageSend messageSend, BlockScope scope) { 52 | String name = charToString(messageSend.selector); 53 | switch (name) { 54 | case "getResultList": 55 | case "getSingleResult": 56 | case "getSingleResultOrNull": 57 | immediatelyCalled = true; 58 | break; 59 | case "count": 60 | case "delete": 61 | case "update": 62 | case "exists": 63 | case "stream": 64 | case "list": 65 | case "find": 66 | // Disable until we can make this type-safe for Javac 67 | // if (messageSend.receiver instanceof SingleNameReference) { 68 | // SingleNameReference ref = (SingleNameReference) messageSend.receiver; 69 | // String target = charToString(ref.token); 70 | // StringLiteral queryArg = firstArgument(messageSend); 71 | // if (queryArg != null) { 72 | // checkPanacheQuery(queryArg, target, name, charToString(queryArg.source()), messageSend.arguments); 73 | // } 74 | if (messageSend.receiver instanceof ThisReference && panacheEntity != null) { 75 | String target = panacheEntity.getSimpleName().toString(); 76 | StringLiteral queryArg = firstArgument(messageSend); 77 | if (queryArg != null) { 78 | String panacheQl = charToString(queryArg.source()); 79 | checkPanacheQuery(queryArg, target, name, panacheQl, messageSend.arguments); 80 | } 81 | } 82 | break; 83 | case "createQuery": 84 | case "createSelectionQuery": 85 | case "createMutationQuery": 86 | for (Expression argument : messageSend.arguments) { 87 | if (argument instanceof StringLiteral) { 88 | check((StringLiteral) argument, true); 89 | } 90 | break; 91 | } 92 | break; 93 | case "setParameter": 94 | for (Expression argument : messageSend.arguments) { 95 | if (argument instanceof StringLiteral) { 96 | String paramName = 97 | charToString(((StringLiteral) argument) 98 | .source()); 99 | setParameterNames.add(paramName); 100 | } else if (argument instanceof IntLiteral) { 101 | int paramLabel = parseInt(new String(((IntLiteral) argument).source())); 102 | setParameterLabels.add(paramLabel); 103 | } 104 | //the remaining parameters aren't parameter ids! 105 | break; 106 | } 107 | 108 | break; 109 | } 110 | return true; 111 | } 112 | 113 | private StringLiteral firstArgument(MessageSend messageSend) { 114 | for (Expression argument : messageSend.arguments) { 115 | if (argument instanceof StringLiteral) { 116 | return (StringLiteral) argument; 117 | } 118 | } 119 | return null; 120 | } 121 | 122 | @Override 123 | public void endVisit(MessageSend messageSend, BlockScope scope) { 124 | String name = charToString(messageSend.selector); 125 | switch (name) { 126 | case "getResultList": 127 | case "getSingleResult": 128 | immediatelyCalled = false; 129 | break; 130 | } 131 | } 132 | 133 | @Override 134 | public boolean visit(MemberValuePair pair, BlockScope scope) { 135 | String qualifiedName = qualifiedName(pair.binding); 136 | if (qualifiedName.equals(jpa("NamedQuery.query")) 137 | || qualifiedName.equals(hibernate("NamedQuery.query")) 138 | || qualifiedName.equals(hibernate("processing.HQL.value"))) { 139 | if (pair.value instanceof StringLiteral) { 140 | check((StringLiteral) pair.value, false); 141 | } 142 | } 143 | return true; 144 | } 145 | 146 | void check(StringLiteral stringLiteral, boolean inCreateQueryMethod) { 147 | String hql = charToString(stringLiteral.source()); 148 | ECJErrorReporter handler = new ECJErrorReporter(stringLiteral, unit, compiler, hql); 149 | validate(hql, inCreateQueryMethod && immediatelyCalled, 150 | setParameterLabels, setParameterNames, handler, 151 | // ProcessorSessionFactory.instance.make(processingEnv)); 152 | ECJProcessor.sessionFactory.make(unit)); 153 | } 154 | 155 | void checkPanacheQuery(StringLiteral stringLiteral, String targetType, String methodName, 156 | String panacheQl, Expression[] args) { 157 | ECJErrorReporter handler = new ECJErrorReporter(stringLiteral, unit, compiler, panacheQl); 158 | collectPanacheArguments(args); 159 | int[] offset = new int[1]; 160 | String hql = PanacheUtils.panacheQlToHql(handler, targetType, methodName, 161 | panacheQl, offset, setParameterLabels, setOrderBy); 162 | if (hql != null) { 163 | validate(hql, true, 164 | setParameterLabels, setParameterNames, handler, 165 | // ProcessorSessionFactory.instance.make(processingEnv), 166 | ECJProcessor.sessionFactory.make(unit), 167 | offset[0]); 168 | } 169 | } 170 | 171 | private void collectPanacheArguments(Expression[] args) { 172 | // first arg is pql 173 | // second arg can be Sort, Object..., Map or Parameters 174 | setParameterLabels.clear(); 175 | setParameterNames.clear(); 176 | setOrderBy.clear(); 177 | if (args.length > 1) { 178 | int firstArgIndex = 1; 179 | if (isPanacheSortCall(args[firstArgIndex])) { 180 | firstArgIndex++; 181 | } 182 | 183 | if (args.length > firstArgIndex) { 184 | Expression firstArg = args[firstArgIndex]; 185 | isParametersCall(firstArg); 186 | if (setParameterNames.isEmpty()) { 187 | for (int i = 0; i < args.length - firstArgIndex; i++) { 188 | setParameterLabels.add(1 + i); 189 | } 190 | } 191 | } 192 | } 193 | } 194 | 195 | private boolean isParametersCall(Expression firstArg) { 196 | if (firstArg instanceof MessageSend) { 197 | MessageSend invocation = (MessageSend) firstArg; 198 | String fieldName = charToString(invocation.selector); 199 | if (fieldName.equals("and") && isParametersCall(invocation.receiver)) { 200 | StringLiteral queryArg = firstArgument(invocation); 201 | if (queryArg != null) { 202 | setParameterNames.add(charToString(queryArg.source())); 203 | return true; 204 | } 205 | } 206 | else if (fieldName.equals("with") 207 | && invocation.receiver instanceof SingleNameReference) { 208 | SingleNameReference receiver = (SingleNameReference) invocation.receiver; 209 | String target = charToString(receiver.token); 210 | if (target.equals("Parameters")) { 211 | StringLiteral queryArg = firstArgument(invocation); 212 | if (queryArg != null) { 213 | setParameterNames.add(charToString(queryArg.source())); 214 | return true; 215 | } 216 | } 217 | } 218 | } 219 | return false; 220 | } 221 | 222 | private boolean isPanacheSortCall(Expression firstArg) { 223 | if (firstArg instanceof MessageSend) { 224 | MessageSend invocation = (MessageSend) firstArg; 225 | String fieldName = charToString(invocation.selector); 226 | if ((fieldName.equals("and") 227 | || fieldName.equals("descending") 228 | || fieldName.equals("ascending") 229 | || fieldName.equals("direction")) 230 | && isPanacheSortCall(invocation.receiver)) { 231 | for (Expression e : invocation.arguments) { 232 | if (e instanceof StringLiteral) { 233 | StringLiteral lit = (StringLiteral) e; 234 | setOrderBy.add(charToString(lit.source())); 235 | } 236 | } 237 | return true; 238 | } 239 | else if ((fieldName.equals("by") 240 | || fieldName.equals("descending") 241 | || fieldName.equals("ascending")) 242 | && invocation.receiver instanceof SingleNameReference) { 243 | SingleNameReference receiver = (SingleNameReference) invocation.receiver; 244 | String target = charToString(receiver.token); 245 | if (target.equals("Sort")) { 246 | for (Expression e : invocation.arguments) { 247 | if (e instanceof StringLiteral) { 248 | StringLiteral lit = (StringLiteral) e; 249 | setOrderBy.add(charToString(lit.source())); 250 | } 251 | } 252 | return true; 253 | } 254 | } 255 | } 256 | return false; 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/ECJErrorReporter.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import org.antlr.v4.runtime.LexerNoViableAltException; 4 | import org.antlr.v4.runtime.Parser; 5 | import org.antlr.v4.runtime.RecognitionException; 6 | import org.antlr.v4.runtime.Recognizer; 7 | import org.antlr.v4.runtime.Token; 8 | import org.antlr.v4.runtime.atn.ATNConfigSet; 9 | import org.antlr.v4.runtime.dfa.DFA; 10 | import org.eclipse.jdt.core.compiler.CategorizedProblem; 11 | import org.eclipse.jdt.internal.compiler.CompilationResult; 12 | import org.eclipse.jdt.internal.compiler.Compiler; 13 | import org.eclipse.jdt.internal.compiler.ast.ASTNode; 14 | import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; 15 | import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities; 16 | 17 | import java.util.BitSet; 18 | 19 | import static org.eclipse.jdt.internal.compiler.util.Util.getLineNumber; 20 | import static org.eclipse.jdt.internal.compiler.util.Util.searchColumnNumber; 21 | import static org.hibernate.query.hql.internal.StandardHqlTranslator.prettifyAntlrError; 22 | 23 | /** 24 | * @author Gavin King 25 | */ 26 | class ECJErrorReporter implements Validation.Handler { 27 | 28 | private final ASTNode node; 29 | private final CompilationUnitDeclaration unit; 30 | private final Compiler compiler; 31 | private final String hql; 32 | private int errorcount; 33 | 34 | ECJErrorReporter(ASTNode node, 35 | CompilationUnitDeclaration unit, 36 | Compiler compiler, 37 | String hql) { 38 | this.node = node; 39 | this.unit = unit; 40 | this.compiler = compiler; 41 | this.hql = hql; 42 | } 43 | 44 | @Override 45 | public int getErrorCount() { 46 | return errorcount; 47 | } 48 | 49 | @Override 50 | public void syntaxError(Recognizer recognizer, Object symbol, int line, int charInLine, String message, RecognitionException e) { 51 | message = prettifyAntlrError(symbol, line, charInLine, message, e, hql, false); 52 | errorcount++; 53 | CompilationResult result = unit.compilationResult(); 54 | char[] fileName = result.fileName; 55 | int[] lineEnds = result.getLineSeparatorPositions(); 56 | int startIndex; 57 | int stopIndex; 58 | int lineNum = getLineNumber(node.sourceStart, lineEnds, 0, lineEnds.length - 1); 59 | Token offendingToken = e.getOffendingToken(); 60 | if (offendingToken != null) { 61 | startIndex = offendingToken.getStartIndex(); 62 | stopIndex = offendingToken.getStopIndex(); 63 | } else if (e instanceof LexerNoViableAltException) { 64 | startIndex = ((LexerNoViableAltException) e).getStartIndex(); 65 | stopIndex = startIndex; 66 | } else { 67 | startIndex = lineEnds[line - 1] + charInLine; 68 | stopIndex = startIndex; 69 | } 70 | int startPosition = node.sourceStart + startIndex + 1; 71 | int endPosition = node.sourceStart + stopIndex + 1; 72 | if (endPosition < startPosition) { 73 | endPosition = startPosition; 74 | } 75 | CategorizedProblem problem = 76 | compiler.problemReporter.problemFactory 77 | .createProblem(fileName, 0, 78 | new String[]{message}, 79 | new String[]{message}, 80 | ProblemSeverities.Error, 81 | startPosition, 82 | endPosition, 83 | lineNum + line - 1, -1); 84 | compiler.problemReporter.record(problem, result, unit, true); 85 | } 86 | 87 | @Override 88 | public void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean b, BitSet bitSet, ATNConfigSet atnConfigSet) { 89 | } 90 | 91 | @Override 92 | public void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitSet, ATNConfigSet atnConfigSet) { 93 | } 94 | 95 | @Override 96 | public void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atnConfigSet) { 97 | } 98 | 99 | @Override 100 | public void error(int start, int end, String message) { 101 | report(ProblemSeverities.Error, message, start, end); 102 | } 103 | 104 | @Override 105 | public void warn(int start, int end, String message) { 106 | report(ProblemSeverities.Warning, message, start, end); 107 | } 108 | 109 | private void report(int severity, String message, int offset, int endOffset) { 110 | errorcount++; 111 | CompilationResult result = unit.compilationResult(); 112 | char[] fileName = result.fileName; 113 | int[] lineEnds = result.getLineSeparatorPositions(); 114 | int startPosition; 115 | int endPosition; 116 | if (node != null) { 117 | startPosition = node.sourceStart + offset; 118 | endPosition = endOffset < 0 ? 119 | node.sourceEnd - 1 : 120 | node.sourceStart + endOffset; 121 | } else { 122 | startPosition = 0; 123 | endPosition = 0; 124 | } 125 | int lineNumber = startPosition >= 0 126 | ? getLineNumber(startPosition, lineEnds, 0, lineEnds.length - 1) 127 | : 0; 128 | int columnNumber = startPosition >= 0 129 | ? searchColumnNumber(lineEnds, lineNumber, startPosition) 130 | : 0; 131 | 132 | CategorizedProblem problem = 133 | compiler.problemReporter.problemFactory 134 | .createProblem(fileName, 0, 135 | new String[]{message}, 136 | new String[]{message}, 137 | severity, 138 | startPosition, endPosition, 139 | lineNumber, columnNumber); 140 | compiler.problemReporter.record(problem, result, unit, true); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/ECJProcessor.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import static org.hibernate.query.validator.ECJSessionFactory.getAnnotation; 4 | import static org.hibernate.query.validator.ECJSessionFactory.qualifiedName; 5 | import static org.hibernate.query.validator.HQLProcessor.CHECK_HQL; 6 | 7 | import java.util.Set; 8 | 9 | import javax.annotation.processing.AbstractProcessor; 10 | import javax.annotation.processing.ProcessingEnvironment; 11 | import javax.annotation.processing.RoundEnvironment; 12 | import javax.lang.model.SourceVersion; 13 | import javax.lang.model.element.TypeElement; 14 | import javax.lang.model.util.Elements; 15 | import javax.tools.Diagnostic; 16 | 17 | import org.eclipse.jdt.internal.compiler.Compiler; 18 | import org.eclipse.jdt.internal.compiler.apt.dispatch.BaseProcessingEnvImpl; 19 | import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; 20 | import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; 21 | import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding; 22 | import org.eclipse.jdt.internal.compiler.lookup.Binding; 23 | import org.eclipse.jdt.internal.compiler.lookup.TypeBinding; 24 | 25 | /** 26 | * Annotation processor that validates HQL and JPQL queries 27 | * for ECJ. 28 | * 29 | * @see org.hibernate.annotations.processing.CheckHQL 30 | * 31 | * @author Gavin King 32 | */ 33 | //@SupportedAnnotationTypes(CHECK_HQL) 34 | public class ECJProcessor extends AbstractProcessor { 35 | 36 | static Mocker sessionFactory = Mocker.variadic(ECJSessionFactory.class); 37 | 38 | @Override 39 | public synchronized void init(ProcessingEnvironment processingEnv) { 40 | super.init(processingEnv); 41 | processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Hibernate Query Validator for ECJ"); 42 | } 43 | 44 | @Override 45 | public boolean process(Set annotations, RoundEnvironment roundEnv) { 46 | Compiler compiler = ((BaseProcessingEnvImpl) processingEnv).getCompiler(); 47 | if (!roundEnv.getRootElements().isEmpty()) { 48 | for (CompilationUnitDeclaration unit : compiler.unitsToProcess) { 49 | compiler.parser.getMethodBodies(unit); 50 | checkHQL(unit, compiler); 51 | } 52 | } 53 | return true; 54 | } 55 | 56 | private void checkHQL(CompilationUnitDeclaration unit, Compiler compiler) { 57 | for (TypeDeclaration type : unit.types) { 58 | if (isCheckable(type.binding, unit)) { 59 | // List whitelist = getWhitelist(type.binding, unit, compiler); 60 | Elements elements = processingEnv.getElementUtils(); 61 | TypeElement typeElement = elements.getTypeElement(qualifiedName(type.binding)); 62 | TypeElement panacheEntity = PanacheUtils.isPanache(typeElement, processingEnv.getTypeUtils(), elements); 63 | type.traverse(new ECJASTVisitor(panacheEntity, unit, compiler, processingEnv), unit.scope); 64 | } 65 | } 66 | } 67 | 68 | private static boolean isCheckable(TypeBinding type, CompilationUnitDeclaration unit) { 69 | return getCheckAnnotation(type, unit)!=null; 70 | } 71 | // 72 | // private static List getWhitelist(TypeBinding type, 73 | // CompilationUnitDeclaration unit, 74 | // Compiler compiler) { 75 | // ElementValuePair[] members = 76 | // getCheckAnnotation(type, unit).getElementValuePairs(); 77 | // if (members==null || members.length==0) { 78 | // return emptyList(); 79 | // } 80 | // List names = new ArrayList<>(); 81 | // for (ElementValuePair pair: members) { 82 | // Object value = pair.value; 83 | // if (value instanceof Object[]) { 84 | // for (Object literal : (Object[]) value) { 85 | // if (literal instanceof StringConstant) { 86 | // names.add(((StringConstant) literal).stringValue()); 87 | // } 88 | // } 89 | // } 90 | // else if (value instanceof StringConstant) { 91 | // names.add(((StringConstant) value).stringValue()); 92 | // } 93 | // else if (value instanceof BinaryTypeBinding) { 94 | //// String name = qualifiedName((BinaryTypeBinding) value); 95 | // names.addAll(MockSessionFactory.functionRegistry.getValidFunctionKeys()); 96 | // } 97 | // } 98 | // return names; 99 | // } 100 | 101 | private static AnnotationBinding getCheckAnnotation(TypeBinding type, 102 | CompilationUnitDeclaration unit) { 103 | AnnotationBinding result = getAnnotation(type, CHECK_HQL); 104 | if (result!=null) return result; 105 | Binding packInfo = unit.scope.getType("package-info".toCharArray()); 106 | return getAnnotation(packInfo, CHECK_HQL); 107 | } 108 | 109 | @Override 110 | public SourceVersion getSupportedSourceVersion() { 111 | return SourceVersion.latestSupported(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/EclipseChecker.groovy: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator 2 | 3 | import static org.hibernate.query.validator.EclipseSessionFactory.getAnnotation 4 | import static org.hibernate.query.validator.HQLProcessor.CHECK_HQL 5 | 6 | /** 7 | * @author Gavin King 8 | */ 9 | class EclipseChecker { 10 | 11 | Set setParameterLabels = new HashSet<>() 12 | Set setParameterNames = new HashSet<>() 13 | Set setOrderBy = new HashSet<>() 14 | boolean immediatelyCalled = false 15 | 16 | private def unit 17 | private def compiler 18 | // private List whitelist 19 | private def processingEnv 20 | 21 | EclipseChecker(unit, compiler, processingEnv) { 22 | this.compiler = compiler 23 | this.unit = unit 24 | this.processingEnv = processingEnv 25 | } 26 | 27 | void checkHQL() { 28 | for (type in unit.types) { 29 | if (isCheckable(type.binding, unit)) { 30 | // whitelist = getWhitelist(type.binding, unit, compiler) 31 | type.annotations.each { annotation -> 32 | switch (EclipseSessionFactory.qualifiedTypeName(annotation.resolvedType)) { 33 | case HQLProcessor.hibernate("processing.HQL"): 34 | annotation.memberValuePairs.each { pair -> 35 | if (EclipseSessionFactory.simpleVariableName(pair) == "value") { 36 | validateArgument(pair.value, false) 37 | } 38 | } 39 | break 40 | case HQLProcessor.jpa("NamedQuery"): 41 | case HQLProcessor.hibernate("NamedQuery"): 42 | annotation.memberValuePairs.each { pair -> 43 | if (EclipseSessionFactory.simpleVariableName(pair) == "query") { 44 | validateArgument(pair.value, false) 45 | } 46 | } 47 | break 48 | case HQLProcessor.jpa("NamedQueries"): 49 | annotation.memberValue.expressions.each { ann -> 50 | ann.memberValuePairs.each { pair -> 51 | if (EclipseSessionFactory.simpleVariableName(pair) == "query") { 52 | validateArgument(pair.value, false) 53 | } 54 | } 55 | } 56 | break 57 | } 58 | } 59 | def elements = processingEnv.getElementUtils() 60 | def typeElement = elements.getTypeElement(qualifiedName(type.binding)) 61 | def panacheEntity = 62 | PanacheUtils.isPanache(typeElement, processingEnv.getTypeUtils(), elements) 63 | type.methods.each { method -> 64 | validateStatements(type, panacheEntity, method.statements) 65 | } 66 | } 67 | } 68 | } 69 | 70 | private static String qualifiedName(type) { 71 | String pkgName = charToString(type.qualifiedPackageName()) 72 | String className = charToString(type.qualifiedSourceName()) 73 | return pkgName.isEmpty() ? className : pkgName + "." + className 74 | } 75 | 76 | private void validateStatements(type, panacheEntity, statements) { 77 | statements.each { statement -> validateStatement(type, panacheEntity, statement) } 78 | } 79 | 80 | private void validateStatement(type, panacheEntity, statement) { 81 | if (statement != null) switch (statement.getClass().simpleName) { 82 | case "MessageSend": 83 | boolean ic = immediatelyCalled 84 | def name = EclipseSessionFactory.simpleMethodName(statement) 85 | switch (name) { 86 | case "getResultList": 87 | case "getSingleResult": 88 | case "getSingleResultOrNull": 89 | immediatelyCalled = true 90 | break 91 | case "count": 92 | case "delete": 93 | case "update": 94 | case "exists": 95 | case "stream": 96 | case "list": 97 | case "find": 98 | // Disabled until we find how to support this type-safe in Javac 99 | // if (statement.receiver.getClass().simpleName == "SingleNameReference") { 100 | // def ref = statement.receiver; 101 | // String target = charToString(ref.token); 102 | // def queryArg = firstArgument(statement); 103 | // if (queryArg != null) { 104 | // checkPanacheQuery(queryArg, target, name, charToString(queryArg.source()), statement.arguments) 105 | // } 106 | if (statement.receiver.getClass().simpleName == "ThisReference" && panacheEntity != null) { 107 | String target = panacheEntity.getSimpleName().toString() 108 | def queryArg = firstArgument(statement) 109 | if (queryArg != null) { 110 | checkPanacheQuery(queryArg, target, name, charToString(queryArg.source()), statement.arguments) 111 | } 112 | } 113 | break 114 | case "createQuery": 115 | case "createSelectionQuery": 116 | case "createMutationQuery": 117 | statement.arguments.each { arg -> 118 | if (arg.getClass().simpleName == "StringLiteral" 119 | || arg.getClass().simpleName == "ExtendedStringLiteral") { 120 | validateArgument(arg, true) 121 | } 122 | } 123 | break 124 | case "setParameter": 125 | def arg = statement.arguments.first() 126 | switch (arg.getClass().simpleName) { 127 | case "IntLiteral": 128 | setParameterLabels.add(Integer.parseInt(new String((char[])arg.source()))) 129 | break 130 | case "StringLiteral": 131 | case "ExtendedStringLiteral": 132 | setParameterNames.add(new String((char[])arg.source())) 133 | break 134 | } 135 | break 136 | } 137 | validateStatement(type, panacheEntity, statement.receiver) 138 | setParameterLabels.clear() 139 | setParameterNames.clear() 140 | immediatelyCalled = ic 141 | validateStatements(type, panacheEntity, statement.arguments) 142 | break 143 | case "AbstractVariableDeclaration": 144 | validateStatement(type, panacheEntity, statement.initialization) 145 | break 146 | case "AssertStatement": 147 | validateStatement(type, panacheEntity, statement.assertExpression) 148 | break 149 | case "Block": 150 | validateStatements(type, panacheEntity, statement.statements) 151 | break 152 | case "SwitchStatement": 153 | validateStatement(type, panacheEntity, statement.expression) 154 | validateStatements(type, panacheEntity, statement.statements) 155 | break 156 | case "ForStatement": 157 | validateStatement(type, panacheEntity, statement.action) 158 | break 159 | case "ForeachStatement": 160 | validateStatement(type, panacheEntity, statement.collection) 161 | validateStatement(type, panacheEntity, statement.action) 162 | break 163 | case "DoStatement": 164 | case "WhileStatement": 165 | validateStatement(type, panacheEntity, statement.condition) 166 | validateStatement(type, panacheEntity, statement.action) 167 | break 168 | case "IfStatement": 169 | validateStatement(type, panacheEntity, statement.condition) 170 | validateStatement(type, panacheEntity, statement.thenStatement) 171 | validateStatement(type, panacheEntity, statement.elseStatement) 172 | break 173 | case "TryStatement": 174 | validateStatement(type, panacheEntity, statement.tryBlock) 175 | validateStatements(type, panacheEntity, statement.catchBlocks) 176 | validateStatement(type, panacheEntity, statement.finallyBlock) 177 | break 178 | case "SynchronizedStatement": 179 | validateStatement(type, panacheEntity, statement.expression) 180 | validateStatement(type, panacheEntity, statement.block) 181 | break 182 | case "BinaryExpression": 183 | validateStatement(type, panacheEntity, statement.left) 184 | validateStatement(type, panacheEntity, statement.right) 185 | break 186 | case "UnaryExpression": 187 | case "CastExpression": 188 | case "InstanceOfExpression": 189 | validateStatement(type, panacheEntity, statement.expression) 190 | break 191 | case "ConditionalExpression": 192 | validateStatement(type, panacheEntity, statement.condition) 193 | validateStatement(type, panacheEntity, statement.valueIfTrue) 194 | validateStatement(type, panacheEntity, statement.valueIfFalse) 195 | break 196 | case "LambdaExpression": 197 | validateStatement(type, panacheEntity, statement.body) 198 | break 199 | case "ArrayInitializer": 200 | validateStatements(type, panacheEntity, statement.expressions) 201 | break 202 | case "ArrayAllocationExpression": 203 | validateStatements(type, panacheEntity, statement.initializer) 204 | break 205 | case "Assignment": 206 | validateStatement(type, panacheEntity, statement.lhs) 207 | validateStatement(type, panacheEntity, statement.expression) 208 | break 209 | case "AllocationExpression": 210 | validateStatements(type, panacheEntity, statement.arguments) 211 | break 212 | case "ReturnStatement": 213 | validateStatement(type, panacheEntity, statement.expression) 214 | break 215 | case "ThrowStatement": 216 | validateStatement(type, panacheEntity, statement.exception) 217 | break 218 | case "LabeledStatement": 219 | validateStatement(type, panacheEntity, statement.statement) 220 | break 221 | } 222 | } 223 | 224 | static def firstArgument(messageSend) { 225 | for (argument in messageSend.arguments) { 226 | if (argument.getClass().simpleName == "StringLiteral" || 227 | argument.getClass().simpleName == "ExtendedStringLiteral") { 228 | return argument 229 | } 230 | } 231 | return null 232 | } 233 | 234 | void validateArgument(arg, boolean inCreateQueryMethod) { 235 | String hql = new String((char[]) arg.source()) 236 | EclipseErrorReporter handler = new EclipseErrorReporter(arg, unit, compiler, hql) 237 | Validation.validate(hql, inCreateQueryMethod && immediatelyCalled, 238 | setParameterLabels, setParameterNames, handler, 239 | EclipseProcessor.sessionFactory.make(unit)) 240 | } 241 | 242 | void checkPanacheQuery(stringLiteral, targetType, methodName, panacheQl, args) { 243 | EclipseErrorReporter handler = new EclipseErrorReporter(stringLiteral, unit, compiler, panacheQl) 244 | collectPanacheArguments(args) 245 | int[] offset = new int[1] 246 | String hql = PanacheUtils.panacheQlToHql(handler, targetType, methodName, 247 | panacheQl, offset, setParameterLabels, setOrderBy) 248 | if (hql != null) { 249 | Validation.validate(hql, true, 250 | setParameterLabels, setParameterNames, handler, 251 | EclipseProcessor.sessionFactory.make(unit), offset[0]) 252 | } 253 | } 254 | 255 | static String charToString(char[] charArray) { 256 | if (charArray == null) return null 257 | return new String(charArray) 258 | } 259 | 260 | void collectPanacheArguments(args) { 261 | // first arg is pql 262 | // second arg can be Sort, Object..., Map or Parameters 263 | setParameterLabels.clear() 264 | setParameterNames.clear() 265 | setOrderBy.clear() 266 | if (args.length > 1) { 267 | int firstArgIndex = 1 268 | if (isPanacheSortCall(args[firstArgIndex])) { 269 | firstArgIndex++ 270 | } 271 | 272 | if (args.length > firstArgIndex) { 273 | def firstArg = args[firstArgIndex] 274 | isParametersCall(firstArg) 275 | if (setParameterNames.isEmpty()) { 276 | for (int i = 0 ; i < args.length - firstArgIndex ; i++) { 277 | setParameterLabels.add(1 + i) 278 | } 279 | } 280 | } 281 | } 282 | } 283 | boolean isParametersCall(firstArg) { 284 | if (firstArg.getClass().simpleName == "MessageSend") { 285 | def invocation = firstArg 286 | String fieldName = charToString(invocation.selector) 287 | if (fieldName.equals("and") && isParametersCall(invocation.receiver)) { 288 | def queryArg = firstArgument(invocation) 289 | if (queryArg != null) { 290 | setParameterNames.add(charToString(queryArg.source())) 291 | return true 292 | } 293 | } 294 | else if (fieldName.equals("with") 295 | && invocation.receiver.getClass().simpleName == "SingleNameReference") { 296 | def receiver = invocation.receiver 297 | String target = charToString(receiver.token) 298 | if (target.equals("Parameters")) { 299 | def queryArg = firstArgument(invocation) 300 | if (queryArg != null) { 301 | setParameterNames.add(charToString(queryArg.source())) 302 | return true 303 | } 304 | } 305 | } 306 | } 307 | return false 308 | } 309 | 310 | boolean isPanacheSortCall(firstArg) { 311 | if (firstArg.getClass().simpleName == "MessageSend") { 312 | def invocation = firstArg 313 | String fieldName = charToString(invocation.selector) 314 | if ((fieldName.equals("and") 315 | || fieldName.equals("descending") 316 | || fieldName.equals("ascending") 317 | || fieldName.equals("direction")) 318 | && isPanacheSortCall(invocation.receiver)) { 319 | for (e in invocation.arguments) { 320 | if (e.getClass().simpleName == "StringLiteral") { 321 | setOrderBy.add(charToString(e.source())) 322 | } 323 | } 324 | return true 325 | } 326 | else if ((fieldName.equals("by") 327 | || fieldName.equals("descending") 328 | || fieldName.equals("ascending")) 329 | && invocation.receiver.getClass().simpleName == "SingleNameReference") { 330 | def receiver = invocation.receiver 331 | String target = charToString(receiver.token) 332 | if (target.equals("Sort")) { 333 | for (e in invocation.arguments) { 334 | if (e.getClass().simpleName == "StringLiteral") { 335 | setOrderBy.add(charToString(e.source())) 336 | } 337 | } 338 | return true 339 | } 340 | } 341 | } 342 | return false 343 | } 344 | 345 | private static boolean isCheckable(type, unit) { 346 | return getCheckAnnotation(type, unit)!=null 347 | } 348 | 349 | private static def getCheckAnnotation(type, unit) { 350 | def result = getAnnotation(type, CHECK_HQL) 351 | if (result!=null) return result 352 | def packInfo = unit.scope.getType("package-info".toCharArray()) 353 | return getAnnotation(packInfo, CHECK_HQL) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/EclipseErrorReporter.groovy: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator 2 | 3 | import org.antlr.v4.runtime.LexerNoViableAltException 4 | import org.antlr.v4.runtime.Parser 5 | import org.antlr.v4.runtime.RecognitionException 6 | import org.antlr.v4.runtime.Recognizer 7 | import org.antlr.v4.runtime.Token 8 | import org.antlr.v4.runtime.atn.ATNConfigSet 9 | import org.antlr.v4.runtime.dfa.DFA 10 | import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities 11 | import org.hibernate.query.hql.internal.StandardHqlTranslator 12 | 13 | /** 14 | * @author Gavin King 15 | */ 16 | class EclipseErrorReporter implements Validation.Handler { 17 | 18 | private def node 19 | private def unit 20 | private def compiler 21 | private int errorcount 22 | private final String hql 23 | 24 | EclipseErrorReporter(node, unit, compiler, String hql) { 25 | this.hql = hql 26 | this.compiler = compiler 27 | this.node = node 28 | this.unit = unit 29 | } 30 | 31 | @Override 32 | int getErrorCount() { 33 | return errorcount 34 | } 35 | 36 | @Override 37 | void syntaxError(Recognizer recognizer, Object symbol, int line, int charInLine, String message, RecognitionException e) { 38 | message = StandardHqlTranslator.prettifyAntlrError(symbol, line, charInLine, message, e, hql, false) 39 | errorcount++ 40 | def result = unit.compilationResult() 41 | char[] fileName = result.fileName 42 | int[] lineEnds = result.getLineSeparatorPositions() 43 | int startIndex 44 | int stopIndex 45 | int lineNum = getLineNumber(node.sourceStart, lineEnds, 0, lineEnds.length - 1) 46 | Token offendingToken = e.getOffendingToken() 47 | if ( offendingToken != null ) { 48 | startIndex = offendingToken.getStartIndex() 49 | stopIndex = offendingToken.getStopIndex() 50 | } 51 | else if ( e instanceof LexerNoViableAltException ) { 52 | startIndex = ((LexerNoViableAltException) e).getStartIndex() 53 | stopIndex = startIndex; 54 | } 55 | else { 56 | startIndex = lineEnds[line-1] + charInLine 57 | stopIndex = startIndex 58 | } 59 | int startPosition = node.sourceStart + startIndex + 1 60 | int endPosition = node.sourceStart + stopIndex + 1 61 | if ( endPosition < startPosition ) { 62 | endPosition = startPosition 63 | } 64 | def problem = 65 | compiler.problemReporter.problemFactory 66 | .createProblem(fileName, 0, 67 | new String[]{message}, 68 | new String[]{message}, 69 | ProblemSeverities.Error, 70 | startPosition, 71 | endPosition, 72 | lineNum+line-1, -1) 73 | compiler.problemReporter.record(problem, result, unit, true) 74 | } 75 | 76 | @Override 77 | void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean b, BitSet bitSet, ATNConfigSet atnConfigSet) { 78 | } 79 | 80 | @Override 81 | void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitSet, ATNConfigSet atnConfigSet) { 82 | } 83 | 84 | @Override 85 | void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atnConfigSet) { 86 | } 87 | 88 | @Override 89 | void error(int start, int end, String message) { 90 | report(1, message, start, end) 91 | } 92 | 93 | @Override 94 | void warn(int start, int end, String message) { 95 | report(0, message, start, end) 96 | } 97 | 98 | private void report(int severity, String message, int offset, int endOffset) { 99 | errorcount++ 100 | def result = unit.compilationResult() 101 | char[] fileName = result.fileName 102 | int[] lineEnds = result.getLineSeparatorPositions() 103 | int startPosition 104 | int endPosition 105 | if (node!=null) { 106 | startPosition = node.sourceStart + offset 107 | endPosition = endOffset < 0 ? 108 | node.sourceEnd - 1 : 109 | node.sourceStart + endOffset 110 | } 111 | else { 112 | startPosition = 0 113 | endPosition = 0 114 | } 115 | int lineNumber = startPosition >= 0 ? 116 | getLineNumber(startPosition, lineEnds, 0, lineEnds.length - 1) : 0 117 | int columnNumber = startPosition >= 0 ? 118 | searchColumnNumber(lineEnds, lineNumber, startPosition) : 0 119 | String[] args = [message] 120 | def problem = 121 | compiler.problemReporter.problemFactory.createProblem( 122 | fileName, 0, 123 | args, args, severity, 124 | startPosition, endPosition, 125 | lineNumber, columnNumber) 126 | compiler.problemReporter.record(problem, result, unit, true) 127 | } 128 | 129 | static int getLineNumber(int position, int[] lineEnds, int g, int d) { 130 | if (lineEnds == null) 131 | return 1 132 | if (d == -1) 133 | return 1 134 | int m = g, start 135 | while (g <= d) { 136 | m = g + (d - g) / 2 137 | if (position < (start = lineEnds[m])) { 138 | d = m - 1 139 | } 140 | else if (position > start) { 141 | g = m + 1 142 | } 143 | else { 144 | return m + 1 145 | } 146 | } 147 | if (position < lineEnds[m]) { 148 | return m + 1 149 | } 150 | return m + 2 151 | } 152 | 153 | static int searchColumnNumber(int[] startLineIndexes, int lineNumber, int position) { 154 | switch (lineNumber) { 155 | case 1: 156 | return position + 1 157 | case 2: 158 | return position - startLineIndexes[0] 159 | default: 160 | int line = lineNumber - 2 161 | int length = startLineIndexes.length 162 | if (line >= length) { 163 | return position - startLineIndexes[length - 1] 164 | } 165 | return position - startLineIndexes[line] 166 | } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/EclipseProcessor.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection GroovyFallthrough 2 | package org.hibernate.query.validator 3 | 4 | 5 | import javax.annotation.processing.AbstractProcessor 6 | import javax.annotation.processing.ProcessingEnvironment 7 | import javax.annotation.processing.RoundEnvironment 8 | import javax.lang.model.SourceVersion 9 | import javax.lang.model.element.TypeElement 10 | import javax.tools.Diagnostic 11 | 12 | /** 13 | * Annotation processor that validates HQL and JPQL queries 14 | * for Eclipse. 15 | * 16 | * @see org.hibernate.annotations.processing.CheckHQL 17 | * 18 | * @author Gavin King 19 | */ 20 | //@SupportedAnnotationTypes(CHECK_HQL) 21 | class EclipseProcessor extends AbstractProcessor { 22 | 23 | private ProcessingEnvironment processingEnv 24 | 25 | synchronized void init(ProcessingEnvironment processingEnv) { 26 | this.processingEnv = processingEnv 27 | super.init(processingEnv) 28 | processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Hibernate Query Validator for Eclipse"); 29 | } 30 | 31 | static Mocker sessionFactory = Mocker.variadic(EclipseSessionFactory.class) 32 | 33 | @Override 34 | boolean process(Set annotations, RoundEnvironment roundEnv) { 35 | def compiler = processingEnv.getCompiler() 36 | if (!roundEnv.getRootElements().isEmpty()) { 37 | for (unit in compiler.unitsToProcess) { 38 | compiler.parser.getMethodBodies(unit) 39 | new EclipseChecker(unit, compiler, processingEnv).checkHQL() 40 | } 41 | } 42 | return false 43 | } 44 | 45 | // private final static String ORG_HIBERNATE = 46 | // new StringBuilder("org.") 47 | // .append("hibernate.") 48 | // .toString() 49 | 50 | // private static String shadow(String name) { 51 | // return name.replace(ORG_HIBERNATE + "dialect", 52 | // ORG_HIBERNATE + "query.validator.hibernate.dialect") 53 | // } 54 | 55 | @Override 56 | SourceVersion getSupportedSourceVersion() { 57 | return SourceVersion.latestSupported() 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/HQLProcessor.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import javax.annotation.processing.AbstractProcessor; 4 | import javax.annotation.processing.ProcessingEnvironment; 5 | import javax.annotation.processing.RoundEnvironment; 6 | import javax.annotation.processing.SupportedAnnotationTypes; 7 | import javax.lang.model.SourceVersion; 8 | import javax.lang.model.element.TypeElement; 9 | import javax.tools.Diagnostic; 10 | 11 | import java.io.PrintWriter; 12 | import java.io.StringWriter; 13 | import java.util.Set; 14 | 15 | /** 16 | * @author Gavin King 17 | */ 18 | @SupportedAnnotationTypes("*") 19 | public class HQLProcessor extends AbstractProcessor { 20 | 21 | static final String CHECK_HQL = hibernate("processing.CheckHQL"); 22 | 23 | static String jpa(String name) { 24 | //sneak it past shadow 25 | return new StringBuilder("jakarta.") 26 | .append("persistence.") 27 | .append(name) 28 | .toString(); 29 | } 30 | 31 | static String hibernate(String name) { 32 | //sneak it past shadow 33 | return new StringBuilder("org.") 34 | .append("hibernate.") 35 | .append("annotations.") 36 | .append(name) 37 | .toString(); 38 | } 39 | public static boolean forceEclipseForTesting = false; 40 | 41 | private AbstractProcessor delegate; 42 | 43 | @Override 44 | public SourceVersion getSupportedSourceVersion() { 45 | return SourceVersion.latestSupported(); 46 | } 47 | 48 | @Override 49 | public synchronized void init(ProcessingEnvironment processingEnv) { 50 | super.init(processingEnv); 51 | String compiler = processingEnv.getClass().getName(); 52 | if (compiler.endsWith("IdeBuildProcessingEnvImpl") 53 | || forceEclipseForTesting) { 54 | //create it using reflection to allow 55 | //us to compile everything else w/o 56 | //the Groovy compiler being present 57 | delegate = newEclipseProcessor(); 58 | } 59 | else if (compiler.endsWith("BatchProcessingEnvImpl")) { 60 | delegate = new ECJProcessor(); 61 | } 62 | else if (compiler.endsWith("JavacProcessingEnvironment")) { 63 | delegate = new JavacProcessor(); 64 | } 65 | if (delegate!=null) { 66 | delegate.init(processingEnv); 67 | } 68 | } 69 | 70 | private static AbstractProcessor newEclipseProcessor() { 71 | try { 72 | return (AbstractProcessor) 73 | Class.forName("org.hibernate.query.validator.EclipseProcessor") 74 | .newInstance(); 75 | } 76 | catch (Exception e) { 77 | e.printStackTrace(); 78 | return null; 79 | } 80 | } 81 | 82 | @Override 83 | public boolean process(Set annotations, RoundEnvironment roundEnv) { 84 | if (delegate==null) { 85 | return false; 86 | } 87 | try { 88 | // processingEnv.getMessager() 89 | // .printMessage(Diagnostic.Kind.MANDATORY_WARNING, 90 | // "CALLED " + roundEnv.getRootElements().size()); 91 | delegate.process(annotations, roundEnv); 92 | } 93 | catch (Throwable e) { 94 | String message = e.getMessage(); 95 | if (message==null) message = e.getClass().getName(); 96 | processingEnv.getMessager() 97 | .printMessage(Diagnostic.Kind.MANDATORY_WARNING, message); 98 | StringWriter writer = new StringWriter(); 99 | e.printStackTrace(new PrintWriter(writer)); 100 | processingEnv.getMessager() 101 | .printMessage(Diagnostic.Kind.MANDATORY_WARNING, writer.toString()); 102 | } 103 | return false; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/JavacChecker.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import com.sun.tools.javac.model.JavacElements; 4 | import com.sun.tools.javac.tree.JCTree; 5 | 6 | import javax.annotation.processing.ProcessingEnvironment; 7 | import javax.lang.model.element.AnnotationMirror; 8 | import javax.lang.model.element.Element; 9 | import javax.lang.model.element.TypeElement; 10 | import javax.lang.model.util.Elements; 11 | 12 | import static org.hibernate.query.validator.HQLProcessor.CHECK_HQL; 13 | 14 | /** 15 | * @author Gavin King 16 | */ 17 | public class JavacChecker { 18 | private final JavacProcessor javacProcessor; 19 | 20 | public JavacChecker(JavacProcessor javacProcessor) { 21 | this.javacProcessor = javacProcessor; 22 | } 23 | 24 | ProcessingEnvironment getProcessingEnv() { 25 | return javacProcessor.getProcessingEnv(); 26 | } 27 | 28 | JavacProcessor getJavacProcessor() { 29 | return javacProcessor; 30 | } 31 | 32 | void checkHQL(Element element) { 33 | Elements elementUtils = getProcessingEnv().getElementUtils(); 34 | if (isCheckable(element) || isCheckable(element.getEnclosingElement())) { 35 | // List whitelist = getWhitelist(element); 36 | JCTree tree = ((JavacElements) elementUtils).getTree(element); 37 | TypeElement panacheEntity = PanacheUtils.isPanache(element, getProcessingEnv().getTypeUtils(), elementUtils); 38 | if (tree != null) { 39 | tree.accept(new JavacTreeScanner(this, element, panacheEntity)); 40 | } 41 | } 42 | } 43 | 44 | private static boolean isCheckAnnotation(AnnotationMirror am) { 45 | return am.getAnnotationType().asElement().toString().equals(CHECK_HQL); 46 | } 47 | 48 | private static boolean isCheckable(Element element) { 49 | for (AnnotationMirror am: element.getAnnotationMirrors()) { 50 | if (isCheckAnnotation(am)) { 51 | return true; 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | // private List getWhitelist(Element element) { 58 | // List list = new ArrayList<>(); 59 | // element.getAnnotationMirrors().forEach(am -> { 60 | // if (isCheckAnnotation(am)) { 61 | // am.getElementValues().forEach((var, act) -> { 62 | // switch (var.getSimpleName().toString()) { 63 | // case "whitelist": 64 | // if (act instanceof Attribute.Array) { 65 | // for (Attribute a: ((Attribute.Array) act).values) { 66 | // Object value = a.getValue(); 67 | // if (value instanceof String) { 68 | // list.add(value.toString()); 69 | // } 70 | // } 71 | // } 72 | // break; 73 | // case "dialect": 74 | // if (act instanceof Attribute.Class) { 75 | // String name = act.getValue().toString().replace(".class",""); 76 | // list.addAll(MockSessionFactory.functionRegistry.getValidFunctionKeys()); 77 | // } 78 | // break; 79 | // } 80 | // }); 81 | // } 82 | // }); 83 | // return list; 84 | // } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/JavacErrorReporter.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import com.sun.tools.javac.model.JavacElements; 4 | import com.sun.tools.javac.tree.JCTree; 5 | import com.sun.tools.javac.util.Context; 6 | import com.sun.tools.javac.util.Log; 7 | import com.sun.tools.javac.util.Pair; 8 | import org.antlr.v4.runtime.LexerNoViableAltException; 9 | import org.antlr.v4.runtime.Parser; 10 | import org.antlr.v4.runtime.RecognitionException; 11 | import org.antlr.v4.runtime.Recognizer; 12 | import org.antlr.v4.runtime.Token; 13 | import org.antlr.v4.runtime.atn.ATNConfigSet; 14 | import org.antlr.v4.runtime.dfa.DFA; 15 | 16 | import javax.lang.model.element.Element; 17 | import javax.tools.JavaFileObject; 18 | 19 | import java.util.BitSet; 20 | 21 | import static com.sun.tools.javac.resources.CompilerProperties.Warnings.ProcMessager; 22 | import static org.hibernate.query.hql.internal.StandardHqlTranslator.prettifyAntlrError; 23 | 24 | /** 25 | * @author Gavin King 26 | */ 27 | class JavacErrorReporter implements Validation.Handler { 28 | 29 | private static final String KEY = "proc.messager"; 30 | 31 | private final Log log; 32 | private final JCTree.JCLiteral literal; 33 | private final String hql; 34 | private int errorcount; 35 | 36 | JavacErrorReporter(JavacProcessor processor, JCTree.JCLiteral literal, Element element, String hql) { 37 | this.literal = literal; 38 | this.hql = hql; 39 | 40 | Context context = processor.getContext(); 41 | log = Log.instance(context); 42 | Pair pair = 43 | JavacElements.instance(context).getTreeAndTopLevel(element, null, null); 44 | JavaFileObject sourcefile = pair == null ? null : pair.snd.sourcefile; 45 | if (sourcefile != null) { 46 | log.useSource(sourcefile); 47 | } 48 | } 49 | 50 | @Override 51 | public int getErrorCount() { 52 | return errorcount; 53 | } 54 | 55 | @Override 56 | public void error(int start, int end, String message) { 57 | errorcount++; 58 | log.error(literal.pos + start, KEY, message); 59 | } 60 | 61 | @Override 62 | public void warn(int start, int end, String message) { 63 | log.warning(literal.pos + start, ProcMessager(message)); 64 | // log.error(literal.pos + start, KEY, message); 65 | } 66 | 67 | @Override 68 | public void syntaxError( 69 | Recognizer recognizer, 70 | Object offendingSymbol, 71 | int line, 72 | int charPositionInLine, 73 | String message, 74 | RecognitionException e) { 75 | message = prettifyAntlrError(offendingSymbol, line, charPositionInLine, message, e, hql, false); 76 | errorcount++; 77 | Token offendingToken = e.getOffendingToken(); 78 | if ( offendingToken != null ) { 79 | log.error(literal.pos+1+offendingToken.getStartIndex(), KEY, message); 80 | } 81 | else if ( e instanceof LexerNoViableAltException ) { 82 | log.error(literal.pos+1+((LexerNoViableAltException) e).getStartIndex(), KEY, message); 83 | } 84 | else { 85 | log.error(literal.pos+1, KEY, message); 86 | } 87 | } 88 | 89 | @Override 90 | public void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean b, BitSet bitSet, ATNConfigSet atnConfigSet) { 91 | } 92 | 93 | @Override 94 | public void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitSet, ATNConfigSet atnConfigSet) { 95 | } 96 | 97 | @Override 98 | public void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atnConfigSet) { 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/JavacProcessor.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import com.sun.tools.javac.processing.JavacProcessingEnvironment; 4 | import com.sun.tools.javac.util.Context; 5 | 6 | import javax.annotation.processing.AbstractProcessor; 7 | import javax.annotation.processing.ProcessingEnvironment; 8 | import javax.annotation.processing.RoundEnvironment; 9 | import javax.lang.model.SourceVersion; 10 | import javax.lang.model.element.Element; 11 | import javax.lang.model.element.PackageElement; 12 | import javax.lang.model.element.TypeElement; 13 | import javax.tools.Diagnostic; 14 | import java.util.Set; 15 | 16 | 17 | /** 18 | * Annotation processor that validates HQL and JPQL queries 19 | * for {@code javac}. 20 | * 21 | * @see org.hibernate.annotations.processing.CheckHQL 22 | * 23 | * @author Gavin King 24 | */ 25 | //@SupportedAnnotationTypes(CHECK_HQL) 26 | public class JavacProcessor extends AbstractProcessor { 27 | 28 | // static Mocker sessionFactory = Mocker.variadic(JavacSessionFactory.class); 29 | 30 | @Override 31 | public synchronized void init(ProcessingEnvironment processingEnv) { 32 | ModularityWorkaround.addOpens(); 33 | super.init(processingEnv); 34 | processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Hibernate Query Validator for Javac"); 35 | } 36 | 37 | ProcessingEnvironment getProcessingEnv() { 38 | return processingEnv; 39 | } 40 | 41 | @Override 42 | public boolean process(Set annotations, RoundEnvironment roundEnv) { 43 | final JavacChecker javacChecker = new JavacChecker(this); 44 | for (Element element : roundEnv.getRootElements()) { 45 | if (element instanceof PackageElement) { 46 | // for (Element member : element.getEnclosedElements()) { 47 | // checkHQL(member); 48 | // } 49 | } 50 | else { 51 | javacChecker.checkHQL(element); 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | @Override 58 | public SourceVersion getSupportedSourceVersion() { 59 | return SourceVersion.latestSupported(); 60 | } 61 | 62 | Context getContext() { 63 | return ((JavacProcessingEnvironment) processingEnv).getContext(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/JavacTreeScanner.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import com.sun.source.tree.ExpressionTree; 4 | import com.sun.source.tree.IdentifierTree; 5 | import com.sun.source.tree.MemberSelectTree; 6 | import com.sun.source.tree.Tree; 7 | import com.sun.tools.javac.tree.JCTree; 8 | import com.sun.tools.javac.tree.TreeScanner; 9 | 10 | import javax.lang.model.element.AnnotationMirror; 11 | import javax.lang.model.element.Element; 12 | import javax.lang.model.element.TypeElement; 13 | import java.util.HashSet; 14 | import java.util.Set; 15 | 16 | import static org.hibernate.query.validator.HQLProcessor.hibernate; 17 | import static org.hibernate.query.validator.HQLProcessor.jpa; 18 | import static org.hibernate.query.validator.Validation.validate; 19 | 20 | /** 21 | * @author Gavin King 22 | */ 23 | class JavacTreeScanner extends TreeScanner { 24 | private final JavacChecker javacChecker; 25 | final Set setParameterLabels; 26 | final Set setParameterNames; 27 | final Set setOrderBy; 28 | private final Element element; 29 | private final TypeElement panacheEntity; 30 | boolean immediatelyCalled; 31 | 32 | public JavacTreeScanner(JavacChecker javacChecker, Element element, TypeElement panacheEntity) { 33 | this.javacChecker = javacChecker; 34 | this.element = element; 35 | this.panacheEntity = panacheEntity; 36 | setParameterLabels = new HashSet<>(); 37 | setParameterNames = new HashSet<>(); 38 | setOrderBy = new HashSet<>(); 39 | } 40 | 41 | private void check(JCTree.JCLiteral jcLiteral, String hql, 42 | boolean inCreateQueryMethod) { 43 | JavacErrorReporter handler = new JavacErrorReporter(javacChecker.getJavacProcessor(), jcLiteral, element, hql); 44 | validate(hql, inCreateQueryMethod && immediatelyCalled, 45 | setParameterLabels, setParameterNames, handler, 46 | ProcessorSessionFactory.instance.make(javacChecker.getProcessingEnv())); 47 | // JavacProcessor.sessionFactory.make(javacChecker.getProcessingEnv())); 48 | } 49 | 50 | private void checkPanacheQuery(JCTree.JCLiteral jcLiteral, String targetType, String methodName, String panacheQl, 51 | com.sun.tools.javac.util.List args) { 52 | JavacErrorReporter handler = 53 | new JavacErrorReporter(javacChecker.getJavacProcessor(), jcLiteral, element, panacheQl); 54 | collectPanacheArguments(args); 55 | int[] offset = new int[1]; 56 | String hql = PanacheUtils.panacheQlToHql(handler, targetType, methodName, 57 | panacheQl, offset, setParameterLabels, setOrderBy); 58 | if (hql == null) 59 | return; 60 | validate(hql, true, 61 | setParameterLabels, setParameterNames, handler, 62 | ProcessorSessionFactory.instance.make(javacChecker.getProcessingEnv()), 63 | offset[0]); 64 | } 65 | 66 | private void collectPanacheArguments(com.sun.tools.javac.util.List args) { 67 | // first arg is pql 68 | // second arg can be Sort, Object..., Map or Parameters 69 | setParameterLabels.clear(); 70 | setParameterNames.clear(); 71 | setOrderBy.clear(); 72 | com.sun.tools.javac.util.List nonQueryArgs = args.tail; 73 | if (!nonQueryArgs.isEmpty()) { 74 | if (isPanacheSortCall(nonQueryArgs.head)) { 75 | nonQueryArgs = nonQueryArgs.tail; 76 | } 77 | 78 | if (!nonQueryArgs.isEmpty()) { 79 | JCTree.JCExpression firstArg = nonQueryArgs.head; 80 | isParametersCall(firstArg); 81 | if (setParameterNames.isEmpty()) { 82 | int i = 1; 83 | for (JCTree.JCExpression arg : nonQueryArgs) { 84 | setParameterLabels.add(i++); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | private boolean isParametersCall(JCTree.JCExpression firstArg) { 92 | if (firstArg.getKind() == Tree.Kind.METHOD_INVOCATION) { 93 | JCTree.JCMethodInvocation invocation = (JCTree.JCMethodInvocation) firstArg; 94 | JCTree.JCExpression method = invocation.meth; 95 | if (method.getKind() == Tree.Kind.MEMBER_SELECT) { 96 | JCTree.JCFieldAccess fa = (JCTree.JCFieldAccess) method; 97 | if (fa.name.toString().equals("and") && isParametersCall(fa.selected)) { 98 | JCTree.JCLiteral queryArg = firstArgument(invocation); 99 | if (queryArg != null && queryArg.value instanceof String) { 100 | String name = (String) queryArg.value; 101 | setParameterNames.add(name); 102 | return true; 103 | } 104 | } else if (fa.name.toString().equals("with") 105 | && fa.selected.getKind() == Tree.Kind.IDENTIFIER) { 106 | String target = ((JCTree.JCIdent) fa.selected).name.toString(); 107 | if (target.equals("Parameters")) { 108 | JCTree.JCLiteral queryArg = firstArgument(invocation); 109 | if (queryArg != null && queryArg.value instanceof String) { 110 | String name = (String) queryArg.value; 111 | setParameterNames.add(name); 112 | return true; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | return false; 119 | } 120 | 121 | private boolean isPanacheSortCall(JCTree.JCExpression firstArg) { 122 | if (firstArg.getKind() == Tree.Kind.METHOD_INVOCATION) { 123 | JCTree.JCMethodInvocation invocation = (JCTree.JCMethodInvocation) firstArg; 124 | JCTree.JCExpression method = invocation.meth; 125 | if (method.getKind() == Tree.Kind.MEMBER_SELECT) { 126 | JCTree.JCFieldAccess fa = (JCTree.JCFieldAccess) method; 127 | String fieldName = fa.name.toString(); 128 | if ((fieldName.equals("and") 129 | || fieldName.equals("descending") 130 | || fieldName.equals("ascending") 131 | || fieldName.equals("direction")) 132 | && isPanacheSortCall(fa.selected)) { 133 | for (JCTree.JCExpression e : invocation.args) { 134 | if (e instanceof JCTree.JCLiteral) { 135 | JCTree.JCLiteral lit = (JCTree.JCLiteral) e; 136 | if (lit.value instanceof String) { 137 | setOrderBy.add((String) lit.value); 138 | } 139 | } 140 | } 141 | return true; 142 | } 143 | else if ((fieldName.equals("by") 144 | || fieldName.equals("descending") 145 | || fieldName.equals("ascending")) 146 | && fa.selected.getKind() == Tree.Kind.IDENTIFIER) { 147 | String target = ((JCTree.JCIdent) fa.selected).name.toString(); 148 | if (target.equals("Sort")) { 149 | for (JCTree.JCExpression e : invocation.args) { 150 | if (e instanceof JCTree.JCLiteral) { 151 | JCTree.JCLiteral lit = (JCTree.JCLiteral) e; 152 | if (lit.value instanceof String) { 153 | setOrderBy.add((String) lit.value); 154 | } 155 | } 156 | } 157 | return true; 158 | } 159 | } 160 | } 161 | } 162 | return false; 163 | } 164 | 165 | private JCTree.JCLiteral firstArgument(JCTree.JCMethodInvocation call) { 166 | for (JCTree.JCExpression e : call.args) { 167 | return e instanceof JCTree.JCLiteral 168 | ? (JCTree.JCLiteral) e 169 | : null; 170 | } 171 | return null; 172 | } 173 | 174 | @Override 175 | public void visitApply(JCTree.JCMethodInvocation jcMethodInvocation) { 176 | String name = getMethodName(jcMethodInvocation.meth); 177 | switch (name) { 178 | case "getResultList": 179 | case "getSingleResult": 180 | case "getSingleResultOrNull": 181 | immediatelyCalled = true; 182 | super.visitApply(jcMethodInvocation); 183 | immediatelyCalled = false; 184 | break; 185 | case "count": 186 | case "delete": 187 | case "update": 188 | case "exists": 189 | case "stream": 190 | case "list": 191 | case "find": 192 | switch (jcMethodInvocation.meth.getKind()) { 193 | // disable this until we figure out how to type the LHS 194 | // case MEMBER_SELECT: 195 | // JCTree.JCFieldAccess fa = (JCFieldAccess) jcMethodInvocation.meth; 196 | // switch (fa.selected.getKind()) { 197 | // case IDENTIFIER: 198 | // JCTree.JCIdent target = (JCIdent) fa.selected; 199 | // JCTree.JCLiteral queryArg = firstArgument(jcMethodInvocation); 200 | // if (queryArg != null && queryArg.value instanceof String) { 201 | // String panacheQl = (String) queryArg.value; 202 | // checkPanacheQuery(queryArg, target.name.toString(), name, panacheQl, jcMethodInvocation.args); 203 | // } 204 | // break; 205 | // } 206 | // break; 207 | case IDENTIFIER: 208 | JCTree.JCLiteral queryArg = firstArgument(jcMethodInvocation); 209 | if (queryArg != null 210 | && queryArg.value instanceof String 211 | && panacheEntity != null) { 212 | String panacheQl = (String) queryArg.value; 213 | String entityName = panacheEntity.getSimpleName().toString(); 214 | checkPanacheQuery(queryArg, entityName, name, panacheQl, jcMethodInvocation.args); 215 | } 216 | break; 217 | } 218 | super.visitApply(jcMethodInvocation); //needed! 219 | break; 220 | case "createQuery": 221 | case "createSelectionQuery": 222 | case "createMutationQuery": 223 | JCTree.JCLiteral queryArg = firstArgument(jcMethodInvocation); 224 | if (queryArg != null && queryArg.value instanceof String) { 225 | String hql = (String) queryArg.value; 226 | check(queryArg, hql, true); 227 | } 228 | super.visitApply(jcMethodInvocation); 229 | break; 230 | case "setParameter": 231 | JCTree.JCLiteral paramArg = firstArgument(jcMethodInvocation); 232 | if (paramArg != null) { 233 | if (paramArg.value instanceof String) { 234 | setParameterNames.add((String) paramArg.value); 235 | } 236 | else if (paramArg.value instanceof Integer) { 237 | setParameterLabels.add((Integer) paramArg.value); 238 | } 239 | } 240 | super.visitApply(jcMethodInvocation); 241 | break; 242 | default: 243 | super.visitApply(jcMethodInvocation); //needed! 244 | break; 245 | } 246 | } 247 | 248 | @Override 249 | public void visitAnnotation(JCTree.JCAnnotation jcAnnotation) { 250 | AnnotationMirror annotation = jcAnnotation.attribute; 251 | String name = annotation.getAnnotationType().toString(); 252 | if (jpa("NamedQuery").equals(name) 253 | || hibernate("NamedQuery").equals(name)) { 254 | for (JCTree.JCExpression arg : jcAnnotation.args) { 255 | if (arg instanceof JCTree.JCAssign) { 256 | JCTree.JCAssign assign = (JCTree.JCAssign) arg; 257 | if ("query".equals(assign.lhs.toString()) 258 | && assign.rhs instanceof JCTree.JCLiteral) { 259 | JCTree.JCLiteral jcLiteral = 260 | (JCTree.JCLiteral) assign.rhs; 261 | if (jcLiteral.value instanceof String) { 262 | check(jcLiteral, (String) jcLiteral.value, false); 263 | } 264 | } 265 | } 266 | } 267 | } 268 | else if (hibernate("processing.HQL").equals(name)) { 269 | for (JCTree.JCExpression arg : jcAnnotation.args) { 270 | if (arg instanceof JCTree.JCAssign) { 271 | JCTree.JCAssign assign = (JCTree.JCAssign) arg; 272 | if ("value".equals(assign.lhs.toString()) 273 | && assign.rhs instanceof JCTree.JCLiteral) { 274 | JCTree.JCLiteral jcLiteral = 275 | (JCTree.JCLiteral) assign.rhs; 276 | if (jcLiteral.value instanceof String) { 277 | check(jcLiteral, (String) jcLiteral.value, false); 278 | } 279 | } 280 | } 281 | } 282 | } 283 | else { 284 | super.visitAnnotation(jcAnnotation); //needed! 285 | } 286 | } 287 | 288 | private static String getMethodName(ExpressionTree select) { 289 | if (select instanceof MemberSelectTree) { 290 | MemberSelectTree ref = (MemberSelectTree) select; 291 | return ref.getIdentifier().toString(); 292 | } 293 | else if (select instanceof IdentifierTree) { 294 | IdentifierTree ref = (IdentifierTree) select; 295 | return ref.getName().toString(); 296 | } 297 | else { 298 | return null; 299 | } 300 | } 301 | 302 | } 303 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/MockCollectionPersister.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import org.hibernate.FetchMode; 4 | import org.hibernate.QueryException; 5 | import org.hibernate.engine.spi.SessionFactoryImplementor; 6 | import org.hibernate.persister.collection.QueryableCollection; 7 | import org.hibernate.persister.entity.EntityPersister; 8 | import org.hibernate.type.CollectionType; 9 | import org.hibernate.type.ListType; 10 | import org.hibernate.type.MapType; 11 | import org.hibernate.type.Type; 12 | 13 | import static org.hibernate.internal.util.StringHelper.root; 14 | import static org.hibernate.query.validator.MockSessionFactory.typeConfiguration; 15 | 16 | /** 17 | * @author Gavin King 18 | */ 19 | public abstract class MockCollectionPersister implements QueryableCollection { 20 | 21 | private static final String[] ID_COLUMN = {"id"}; 22 | private static final String[] INDEX_COLUMN = {"pos"}; 23 | 24 | private final String role; 25 | private final MockSessionFactory factory; 26 | private final CollectionType collectionType; 27 | private final String ownerEntityName; 28 | private final Type elementType; 29 | 30 | public MockCollectionPersister(String role, CollectionType collectionType, Type elementType, MockSessionFactory factory) { 31 | this.role = role; 32 | this.collectionType = collectionType; 33 | this.elementType = elementType; 34 | this.factory = factory; 35 | this.ownerEntityName = root(role); 36 | } 37 | 38 | String getOwnerEntityName() { 39 | return ownerEntityName; 40 | } 41 | 42 | @Override 43 | public String getRole() { 44 | return role; 45 | } 46 | 47 | @Override 48 | public String getName() { 49 | return role; 50 | } 51 | 52 | @Override 53 | public CollectionType getCollectionType() { 54 | return collectionType; 55 | } 56 | 57 | @Override 58 | public EntityPersister getOwnerEntityPersister() { 59 | return factory.getMetamodel().entityPersister(ownerEntityName); 60 | } 61 | 62 | abstract Type getElementPropertyType(String propertyPath); 63 | 64 | @Override 65 | public Type toType(String propertyName) throws QueryException { 66 | if ("index".equals(propertyName)) { 67 | //this is what AbstractCollectionPersister does! 68 | //TODO: move it to FromElementType:626 or all 69 | // the way to CollectionPropertyMapping 70 | return getIndexType(); 71 | } 72 | Type type = getElementPropertyType(propertyName); 73 | if (type==null) { 74 | throw new QueryException(elementType.getName() 75 | + " has no mapped " 76 | + propertyName); 77 | } 78 | else { 79 | return type; 80 | } 81 | } 82 | 83 | @Override 84 | public Type getKeyType() { 85 | return getOwnerEntityPersister().getIdentifierType(); 86 | } 87 | 88 | @Override 89 | public Type getIndexType() { 90 | if (collectionType instanceof ListType) { 91 | return typeConfiguration.getBasicTypeForJavaType(Integer.class); 92 | } 93 | else if (collectionType instanceof MapType) { 94 | //TODO!!! this is incorrect, return the correct key type 95 | return typeConfiguration.getBasicTypeForJavaType(String.class); 96 | } 97 | else { 98 | return null; 99 | } 100 | } 101 | 102 | @Override 103 | public Type getElementType() { 104 | return elementType; 105 | } 106 | 107 | @Override 108 | public Type getIdentifierType() { 109 | return typeConfiguration.getBasicTypeForJavaType(Long.class); 110 | } 111 | 112 | @Override 113 | public boolean hasIndex() { 114 | return getCollectionType() instanceof ListType 115 | || getCollectionType() instanceof MapType; 116 | } 117 | 118 | @Override 119 | public EntityPersister getElementPersister() { 120 | if (elementType.isEntityType()) { 121 | return factory.getMetamodel() 122 | .entityPersister(elementType.getName()); 123 | } 124 | else { 125 | return null; 126 | } 127 | } 128 | 129 | @Override 130 | public SessionFactoryImplementor getFactory() { 131 | return factory; 132 | } 133 | 134 | @Override 135 | public boolean isOneToMany() { 136 | return elementType.isEntityType(); 137 | } 138 | 139 | @Override 140 | public String[] getCollectionSpaces() { 141 | return new String[] {role}; 142 | } 143 | 144 | @Override 145 | public String getMappedByProperty() { 146 | return null; 147 | } 148 | 149 | @Override 150 | public String[] getIndexColumnNames() { 151 | return INDEX_COLUMN; 152 | } 153 | 154 | @Override 155 | public String[] getIndexColumnNames(String alias) { 156 | return INDEX_COLUMN; 157 | } 158 | 159 | @Override 160 | public String[] getIndexFormulas() { 161 | return null; 162 | } 163 | 164 | @Override 165 | public String[] getElementColumnNames(String alias) { 166 | return new String[] {""}; 167 | } 168 | 169 | @Override 170 | public String[] getElementColumnNames() { 171 | return new String[] {""}; 172 | } 173 | 174 | @Override 175 | public FetchMode getFetchMode() { 176 | return FetchMode.DEFAULT; 177 | } 178 | 179 | @Override 180 | public String getTableName() { 181 | return role; 182 | } 183 | 184 | @Override 185 | public String[] getKeyColumnNames() { 186 | return ID_COLUMN; 187 | } 188 | 189 | @Override 190 | public boolean isCollection() { 191 | return true; 192 | } 193 | 194 | @Override 195 | public boolean consumesCollectionAlias() { 196 | return true; 197 | } 198 | 199 | @Override 200 | public String[] toColumns(String propertyName) { 201 | return new String[] {""}; 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/MockEntityPersister.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import jakarta.persistence.AccessType; 4 | import org.hibernate.QueryException; 5 | import org.hibernate.engine.spi.SessionFactoryImplementor; 6 | import org.hibernate.persister.entity.DiscriminatorMetadata; 7 | import org.hibernate.persister.entity.EntityPersister; 8 | import org.hibernate.persister.entity.Queryable; 9 | import org.hibernate.tuple.entity.EntityMetamodel; 10 | import org.hibernate.type.Type; 11 | 12 | import java.io.Serializable; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.HashSet; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | import java.util.Set; 20 | 21 | import static org.hibernate.query.validator.MockSessionFactory.typeConfiguration; 22 | 23 | /** 24 | * @author Gavin King 25 | */ 26 | public abstract class MockEntityPersister implements EntityPersister, Queryable, DiscriminatorMetadata { 27 | 28 | private static final String[] ID_COLUMN = {"id"}; 29 | 30 | private final String entityName; 31 | private final MockSessionFactory factory; 32 | private final List subclassPersisters = new ArrayList<>(); 33 | final AccessType defaultAccessType; 34 | private final Map propertyTypesByName = new HashMap<>(); 35 | 36 | public MockEntityPersister(String entityName, AccessType defaultAccessType, MockSessionFactory factory) { 37 | this.entityName = entityName; 38 | this.factory = factory; 39 | this.defaultAccessType = defaultAccessType; 40 | } 41 | 42 | void initSubclassPersisters() { 43 | for (MockEntityPersister other: factory.getMockEntityPersisters()) { 44 | other.addPersister(this); 45 | this.addPersister(other); 46 | } 47 | } 48 | 49 | private void addPersister(MockEntityPersister entityPersister) { 50 | if (isSubclassPersister(entityPersister)) { 51 | subclassPersisters.add(entityPersister); 52 | } 53 | } 54 | 55 | private Type getSubclassPropertyType(String propertyPath) { 56 | return subclassPersisters.stream() 57 | .map(sp -> sp.getPropertyType(propertyPath)) 58 | .filter(Objects::nonNull) 59 | .findAny() 60 | .orElse(null); 61 | } 62 | 63 | abstract boolean isSubclassPersister(MockEntityPersister entityPersister); 64 | 65 | @Override 66 | public boolean isSubclassEntityName(String name) { 67 | return isSubclassPersister(subclassPersisters.stream() 68 | .filter(persister -> persister.entityName.equals(name)) 69 | .findFirst().get()); 70 | } 71 | 72 | @Override 73 | public SessionFactoryImplementor getFactory() { 74 | return factory; 75 | } 76 | 77 | @Override 78 | public EntityMetamodel getEntityMetamodel() { 79 | throw new UnsupportedOperationException(); 80 | } 81 | 82 | @Override 83 | public String getEntityName() { 84 | return entityName; 85 | } 86 | 87 | @Override 88 | public String getName() { 89 | return entityName; 90 | } 91 | 92 | @Override 93 | public final Type getPropertyType(String propertyPath) { 94 | Type result = propertyTypesByName.get(propertyPath); 95 | if (result!=null) { 96 | return result; 97 | } 98 | 99 | result = createPropertyType(propertyPath); 100 | if (result == null) { 101 | //check subclasses, needed for treat() 102 | result = getSubclassPropertyType(propertyPath); 103 | } 104 | 105 | if (result!=null) { 106 | propertyTypesByName.put(propertyPath, result); 107 | } 108 | return result; 109 | } 110 | 111 | abstract Type createPropertyType(String propertyPath); 112 | 113 | @Override 114 | public Type getIdentifierType() { 115 | //TODO: propertyType(getIdentifierPropertyName()) 116 | return typeConfiguration.getBasicTypeForJavaType(Long.class); 117 | } 118 | 119 | @Override 120 | public String getIdentifierPropertyName() { 121 | //TODO: return the correct @Id property name 122 | return "id"; 123 | } 124 | 125 | @Override 126 | public Type toType(String propertyName) throws QueryException { 127 | Type type = getPropertyType(propertyName); 128 | if (type == null) { 129 | throw new QueryException(getEntityName() 130 | + " has no mapped " 131 | + propertyName); 132 | } 133 | return type; 134 | } 135 | 136 | @Override 137 | public String getRootEntityName() { 138 | for (MockEntityPersister persister : factory.getMockEntityPersisters()) { 139 | if (this != persister && persister.isSubclassPersister(this)) { 140 | return persister.getRootEntityName(); 141 | } 142 | } 143 | return entityName; 144 | } 145 | 146 | @Override 147 | public Set getSubclassEntityNames() { 148 | Set names = new HashSet<>(); 149 | names.add( entityName ); 150 | for (MockEntityPersister persister : factory.getMockEntityPersisters()) { 151 | if (persister.isSubclassPersister(this)) { 152 | names.add(persister.entityName); 153 | } 154 | } 155 | return names; 156 | } 157 | 158 | @Override 159 | public Declarer getSubclassPropertyDeclarer(String s) { 160 | return Declarer.CLASS; 161 | } 162 | 163 | @Override 164 | public String[] toColumns(String propertyName) { 165 | return new String[] { "" }; 166 | } 167 | 168 | @Override 169 | public String[] getPropertySpaces() { 170 | return new String[] {entityName}; 171 | } 172 | 173 | @Override 174 | public Serializable[] getQuerySpaces() { 175 | return new Serializable[] {entityName}; 176 | } 177 | 178 | @Override 179 | public EntityPersister getEntityPersister() { 180 | return this; 181 | } 182 | 183 | @Override 184 | public String[] getKeyColumnNames() { 185 | return getIdentifierColumnNames(); 186 | } 187 | 188 | @Override 189 | public String[] getIdentifierColumnNames() { 190 | return ID_COLUMN; 191 | } 192 | 193 | @Override 194 | public DiscriminatorMetadata getTypeDiscriminatorMetadata() { 195 | return this; 196 | } 197 | 198 | @Override 199 | public Type getResolutionType() { 200 | return typeConfiguration.getBasicTypeForJavaType(Class.class); 201 | } 202 | 203 | @Override 204 | public String getTableName() { 205 | return entityName; 206 | } 207 | 208 | @Override 209 | public String toString() { 210 | return "MockEntityPersister[" + entityName + "]"; 211 | } 212 | 213 | @Override 214 | public int getVersionProperty() { 215 | return -66; 216 | } 217 | 218 | @Override 219 | public String getMappedSuperclass() { 220 | return null; 221 | } 222 | 223 | @Override 224 | public boolean consumesEntityAlias() { 225 | return true; 226 | } 227 | 228 | @Override 229 | public Type getDiscriminatorType() { 230 | return typeConfiguration.getBasicTypeForJavaType(String.class); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/MockJdbcServicesInitiator.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import org.hibernate.annotations.processing.GenericDialect; 4 | import org.hibernate.boot.model.naming.Identifier; 5 | import org.hibernate.dialect.Dialect; 6 | import org.hibernate.engine.jdbc.env.internal.QualifiedObjectNameFormatterStandardImpl; 7 | import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; 8 | import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; 9 | import org.hibernate.engine.jdbc.env.spi.QualifiedObjectNameFormatter; 10 | import org.hibernate.engine.jdbc.internal.JdbcServicesInitiator; 11 | import org.hibernate.engine.jdbc.spi.JdbcServices; 12 | import org.hibernate.service.spi.ServiceRegistryImplementor; 13 | import org.hibernate.sql.ast.SqlAstTranslatorFactory; 14 | import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; 15 | 16 | import java.util.Map; 17 | 18 | /** 19 | * @author Gavin King 20 | */ 21 | class MockJdbcServicesInitiator extends JdbcServicesInitiator { 22 | 23 | static final JdbcServicesInitiator INSTANCE = new MockJdbcServicesInitiator(); 24 | 25 | static final JdbcServices jdbcServices = Mocker.nullary(MockJdbcServices.class).get(); 26 | static final GenericDialect genericDialect = new GenericDialect(); 27 | 28 | public abstract static class MockJdbcServices implements JdbcServices, JdbcEnvironment { 29 | @Override 30 | public Dialect getDialect() { 31 | return genericDialect; 32 | } 33 | 34 | @Override 35 | public JdbcEnvironment getJdbcEnvironment() { 36 | return this; 37 | } 38 | 39 | @Override 40 | public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { 41 | return new StandardSqlAstTranslatorFactory(); 42 | } 43 | 44 | @Override 45 | public Identifier getCurrentCatalog() { 46 | return null; 47 | } 48 | 49 | @Override 50 | public Identifier getCurrentSchema() { 51 | return null; 52 | } 53 | 54 | @Override 55 | public QualifiedObjectNameFormatter getQualifiedObjectNameFormatter() { 56 | return new QualifiedObjectNameFormatterStandardImpl(getNameQualifierSupport()); 57 | } 58 | 59 | @Override 60 | public NameQualifierSupport getNameQualifierSupport() { 61 | return genericDialect.getNameQualifierSupport(); 62 | } 63 | } 64 | 65 | @Override 66 | public JdbcServices initiateService(Map configurationValues, ServiceRegistryImplementor registry) { 67 | return jdbcServices; 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/Mocker.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import net.bytebuddy.ByteBuddy; 4 | 5 | import java.lang.reflect.Constructor; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.function.Supplier; 9 | 10 | import static net.bytebuddy.implementation.FixedValue.value; 11 | import static net.bytebuddy.matcher.ElementMatchers.isAbstract; 12 | import static net.bytebuddy.matcher.ElementMatchers.returns; 13 | 14 | @FunctionalInterface 15 | public interface Mocker { 16 | 17 | T make(Object... args); 18 | 19 | Map, Class> mocks = new HashMap<>(); 20 | 21 | static Supplier nullary(Class clazz) { 22 | try { 23 | Class mock = load(clazz); 24 | return () -> { 25 | try { 26 | return mock.newInstance(); 27 | } 28 | catch (Exception e) { 29 | throw new RuntimeException(e); 30 | } 31 | }; 32 | } 33 | catch (Exception e) { 34 | throw new RuntimeException(e); 35 | } 36 | } 37 | 38 | @SuppressWarnings("unchecked") 39 | static Mocker variadic(Class clazz) { 40 | Constructor[] constructors = load(clazz).getDeclaredConstructors(); 41 | if (constructors.length>1) { 42 | throw new RuntimeException("more than one constructor for " + clazz); 43 | } 44 | Constructor constructor = constructors[0]; 45 | return (args) -> { 46 | try { 47 | return (T) constructor.newInstance(args); 48 | } 49 | catch (Exception e) { 50 | throw new RuntimeException(e); 51 | } 52 | }; 53 | } 54 | 55 | @SuppressWarnings("unchecked") 56 | static Class load(Class clazz) { 57 | if (mocks.containsKey(clazz)) { 58 | return (Class) mocks.get(clazz); 59 | } 60 | Class mock = 61 | new ByteBuddy() 62 | .subclass(clazz) 63 | .method(returns(String.class).and(isAbstract())) 64 | .intercept(value("")) 65 | .method(returns(boolean.class).and(isAbstract())) 66 | .intercept(value(false)) 67 | .method(returns(int.class).and(isAbstract())) 68 | .intercept(value(0)) 69 | .method(returns(long.class).and(isAbstract())) 70 | .intercept(value(0L)) 71 | .method(returns(int[].class).and(isAbstract())) 72 | .intercept(value(new int[0])) 73 | .method(returns(String[].class).and(isAbstract())) 74 | .intercept(value(new String[0])) 75 | .make() 76 | .load(clazz.getClassLoader()) 77 | .getLoaded(); 78 | mocks.put(clazz,mock); 79 | return mock; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/ModularityWorkaround.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2021 The Project Lombok Authors. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | package org.hibernate.query.validator; 23 | 24 | import sun.misc.Unsafe; 25 | 26 | import java.lang.reflect.Field; 27 | import java.lang.reflect.Method; 28 | 29 | /** 30 | * COPY/PASTE from Lombok! 31 | */ 32 | public class ModularityWorkaround { 33 | 34 | private static Object getJdkCompilerModule() { 35 | /* call public api: ModuleLayer.boot().findModule("jdk.compiler").get(); 36 | but use reflection because we don't want this code to crash on jdk1.7 and below. 37 | In that case, none of this stuff was needed in the first place, so we just exit via 38 | the catch block and do nothing. 39 | */ 40 | 41 | try { 42 | Class cModuleLayer = Class.forName("java.lang.ModuleLayer"); 43 | Method mBoot = cModuleLayer.getDeclaredMethod("boot"); 44 | Object bootLayer = mBoot.invoke(null); 45 | Class cOptional = Class.forName("java.util.Optional"); 46 | Method mFindModule = cModuleLayer.getDeclaredMethod("findModule", String.class); 47 | Object oCompilerO = mFindModule.invoke(bootLayer, "jdk.compiler"); 48 | return cOptional.getDeclaredMethod("get").invoke(oCompilerO); 49 | } catch (Exception e) { 50 | return null; 51 | } 52 | } 53 | 54 | /** 55 | * Useful from jdk9 and up; required from jdk16 and up. 56 | * This code is supposed to gracefully do nothing on jdk8 57 | * and below, as this operation isn't needed there. */ 58 | public static void addOpens() { 59 | Class cModule; 60 | try { 61 | cModule = Class.forName("java.lang.Module"); 62 | } catch (ClassNotFoundException e) { 63 | return; //jdk8-; this is not needed. 64 | } 65 | 66 | Unsafe unsafe = getUnsafe(); 67 | Object jdkCompilerModule = getJdkCompilerModule(); 68 | Object ownModule = getOwnModule(); 69 | String[] allPkgs = { 70 | "com.sun.tools.javac.code", 71 | "com.sun.tools.javac.model", 72 | "com.sun.tools.javac.processing", 73 | "com.sun.tools.javac.tree", 74 | "com.sun.tools.javac.util", 75 | "com.sun.tools.javac.resources" 76 | }; 77 | 78 | try { 79 | Method m = cModule.getDeclaredMethod("implAddOpens", String.class, cModule); 80 | long firstFieldOffset = getFirstFieldOffset(unsafe); 81 | unsafe.putBooleanVolatile(m, firstFieldOffset, true); 82 | for (String p : allPkgs) { 83 | m.invoke(jdkCompilerModule, p, ownModule); 84 | } 85 | } catch (Exception ignore) {} 86 | } 87 | 88 | private static long getFirstFieldOffset(Unsafe unsafe) { 89 | try { 90 | return unsafe.objectFieldOffset(Parent.class.getDeclaredField("first")); 91 | } catch (NoSuchFieldException e) { 92 | // can't happen. 93 | throw new RuntimeException(e); 94 | } catch (SecurityException e) { 95 | // can't happen 96 | throw new RuntimeException(e); 97 | } 98 | } 99 | 100 | private static Unsafe getUnsafe() { 101 | try { 102 | Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); 103 | theUnsafe.setAccessible(true); 104 | return (Unsafe) theUnsafe.get(null); 105 | } catch (Exception e) { 106 | return null; 107 | } 108 | } 109 | 110 | private static Object getOwnModule() { 111 | try { 112 | Method m = Permit.getMethod(Class.class, "getModule"); 113 | return m.invoke(JavacProcessor.class); 114 | } catch (Exception e) { 115 | return null; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/PanacheUtils.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import java.util.List; 4 | import java.util.Set; 5 | 6 | import javax.lang.model.element.Element; 7 | import javax.lang.model.element.ElementKind; 8 | import javax.lang.model.element.TypeElement; 9 | import javax.lang.model.type.DeclaredType; 10 | import javax.lang.model.type.TypeKind; 11 | import javax.lang.model.type.TypeMirror; 12 | import javax.lang.model.util.Elements; 13 | import javax.lang.model.util.Types; 14 | 15 | public class PanacheUtils { 16 | public static String panacheQlToHql(Validation.Handler handler, String targetType, String methodName, 17 | String panacheQl, int[] offset, Set setParameterLabels, 18 | Set setOrderBy) { 19 | String ret = null; 20 | switch (methodName) { 21 | case "find": 22 | case "list": 23 | case "stream": 24 | ret = panacheFindQueryToHql(handler, targetType, panacheQl, offset, setParameterLabels); 25 | break; 26 | // FIXME: those throw: 27 | /* 28 | warning: Method org/hibernate/query/validator/JavacSessionFactory$EntityPersister$ByteBuddy$BYYHBkRf.getDiscriminatorType()Lorg/hibernate/type/Type; is abstract 29 | warning: java.lang.AbstractMethodError: Method org/hibernate/query/validator/JavacSessionFactory$EntityPersister$ByteBuddy$BYYHBkRf.getDiscriminatorType()Lorg/hibernate/type/Type; is abstract 30 | at org.hibernate.query.validator.JavacSessionFactory$EntityPersister$ByteBuddy$BYYHBkRf.getDiscriminatorType(Unknown Source) 31 | at org.hibernate.hql.internal.ast.HqlSqlWalker.postProcessDML(HqlSqlWalker.java:840) 32 | at org.hibernate.hql.internal.ast.HqlSqlWalker.postProcessUpdate(HqlSqlWalker.java:855) 33 | at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.updateStatement(HqlSqlBaseWalker.java:417) 34 | at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.statement(HqlSqlBaseWalker.java:273) 35 | at org.hibernate.query.validator.Validation.validate(Validation.java:77) 36 | at org.hibernate.query.validator.JavacProcessor$1.checkPanacheQuery(JavacProcessor.java:97) 37 | at org.hibernate.query.validator.JavacProcessor$1.visitApply(JavacProcessor.java:374) 38 | */ 39 | case "delete": 40 | return panacheDeleteQueryToHql(handler, targetType, panacheQl, offset, setParameterLabels); 41 | case "update": 42 | return panacheUpdateQueryToHql(handler, targetType, panacheQl, offset, setParameterLabels); 43 | case "exists": 44 | case "count": 45 | ret = panacheCountQueryToHql(handler, targetType, panacheQl, offset, setParameterLabels); 46 | break; 47 | } 48 | if (ret != null && !setOrderBy.isEmpty()) { 49 | ret += " ORDER BY "+String.join(", ", setOrderBy); 50 | } 51 | return ret; 52 | } 53 | 54 | private static String panacheCountQueryToHql(Validation.Handler handler, String targetType, 55 | String query, int[] offset, Set setParameterLabels) { 56 | String countPrefix = "SELECT COUNT(*) "; 57 | String fromPrefix = "FROM "+targetType; 58 | if (query == null) { 59 | offset[0] = countPrefix.length()+fromPrefix.length(); 60 | return countPrefix + fromPrefix; 61 | } 62 | 63 | String trimmed = query.trim(); 64 | if (trimmed.isEmpty()) { 65 | offset[0] = countPrefix.length()+fromPrefix.length(); 66 | return countPrefix + fromPrefix; 67 | } 68 | 69 | String trimmedLc = trimmed.toLowerCase(); 70 | if (trimmedLc.startsWith("from ")) { 71 | offset[0] = countPrefix.length(); 72 | return countPrefix + query; 73 | } 74 | if (trimmedLc.startsWith("order by ")) { 75 | // ignore it 76 | offset[0] = countPrefix.length()+fromPrefix.length(); 77 | return countPrefix + fromPrefix; 78 | } 79 | if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1) { 80 | if (missingRequiredSingleParam(handler, query, setParameterLabels)) 81 | return null; 82 | query += " = ?1"; 83 | } 84 | offset[0] = countPrefix.length()+fromPrefix.length() + 7; 85 | return countPrefix + fromPrefix + " WHERE "+query; 86 | } 87 | 88 | private static String panacheUpdateQueryToHql(Validation.Handler handler, String targetType, 89 | String query, int[] offset, Set setParameterLabels) { 90 | if (query == null) { 91 | return null; 92 | } 93 | 94 | String trimmed = query.trim(); 95 | if (trimmed.isEmpty()) { 96 | return null; 97 | } 98 | 99 | String trimmedLc = trimmed.toLowerCase(); 100 | if (trimmedLc.startsWith("update ")) { 101 | offset[0] = 0; 102 | return query; 103 | } 104 | if (trimmedLc.startsWith("from ")) { 105 | offset[0] = 7; 106 | return "UPDATE " + query; 107 | } 108 | if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1) { 109 | if (missingRequiredSingleParam(handler, query, setParameterLabels)) 110 | return null; 111 | query += " = ?1"; 112 | } 113 | String fromPrefix = "UPDATE " + targetType + " "; 114 | if (trimmedLc.startsWith("set ")) { 115 | offset[0] = fromPrefix.length(); 116 | return fromPrefix + query; 117 | } 118 | offset[0] = fromPrefix.length() + 4; 119 | return fromPrefix + "SET " + query; 120 | } 121 | 122 | private static String panacheDeleteQueryToHql(Validation.Handler handler, String targetType, 123 | String query, int[] offset, Set setParameterLabels) { 124 | String deletePrefix = "DELETE FROM " + targetType; 125 | if (query == null) { 126 | offset[0] = deletePrefix.length(); 127 | return deletePrefix; 128 | } 129 | 130 | String trimmed = query.trim(); 131 | if (trimmed.isEmpty()) { 132 | offset[0] = deletePrefix.length(); 133 | return deletePrefix; 134 | } 135 | 136 | String trimmedLc = trimmed.toLowerCase(); 137 | if (trimmedLc.startsWith("from ")) { 138 | offset[0] = 7; 139 | return "DELETE " + query; 140 | } 141 | if (trimmedLc.startsWith("order by ")) { 142 | // ignore it 143 | offset[0] = deletePrefix.length(); 144 | return deletePrefix; 145 | } 146 | if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1) { 147 | if (missingRequiredSingleParam(handler, query, setParameterLabels)) 148 | return null; 149 | query += " = ?1"; 150 | } 151 | offset[0] = deletePrefix.length() + 7; 152 | return deletePrefix + " WHERE " + query; 153 | } 154 | 155 | private static String panacheFindQueryToHql(Validation.Handler handler, String targetType, 156 | String query, int[] offset, Set setParameterLabels) { 157 | String fromPrefix = "FROM " + targetType; 158 | if (query == null) { 159 | offset[0] = fromPrefix.length(); 160 | return fromPrefix; 161 | } 162 | 163 | String trimmed = query.trim(); 164 | if (trimmed.isEmpty()) { 165 | offset[0] = fromPrefix.length(); 166 | return fromPrefix; 167 | } 168 | 169 | // FIXME: support, by collecting and checking named queries 170 | if (isNamedQuery(query)) { 171 | return null; 172 | } 173 | 174 | String trimmedLc = trimmed.toLowerCase(); 175 | if (trimmedLc.startsWith("from ") || trimmedLc.startsWith("select ")) { 176 | offset[0] = 0; 177 | return query; 178 | } 179 | if (trimmedLc.startsWith("order by ")) { 180 | offset[0] = fromPrefix.length()+1; 181 | return fromPrefix + " " + query; 182 | } 183 | if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1) { 184 | if (missingRequiredSingleParam(handler, query, setParameterLabels)) 185 | return null; 186 | query += " = ?1"; 187 | } 188 | offset[0] = fromPrefix.length() + 7; 189 | return fromPrefix + " WHERE " + query; 190 | } 191 | 192 | private static boolean isNamedQuery(String query) { 193 | return query != null && !query.isEmpty() && query.charAt(0) == '#'; 194 | } 195 | 196 | private static boolean missingRequiredSingleParam(Validation.Handler handler, String query, 197 | Set setParameterLabels) { 198 | if (setParameterLabels.size() < 1) { 199 | handler.warn(0, 0, "Missing required argument for '" + query + "'"); 200 | return true; 201 | } 202 | else if (setParameterLabels.size() > 1){ 203 | handler.warn(0, 0, "Too many arguments for '" + query + "'"); 204 | return true; 205 | } 206 | return false; 207 | } 208 | 209 | public static TypeElement isPanache(Element element, Types types, Elements elements) { 210 | if (element.getKind() == ElementKind.CLASS 211 | || element.getKind() == ElementKind.INTERFACE) { 212 | TypeMirror type = element.asType(); 213 | TypeElement panacheEntityType = elements.getTypeElement("io.quarkus.hibernate.orm.panache.PanacheEntityBase"); 214 | if (panacheEntityType == null) 215 | return null; 216 | if (types.isSubtype(type, panacheEntityType.asType())) 217 | return (TypeElement) types.asElement(type); 218 | TypeElement panacheRepositoryType = elements.getTypeElement("io.quarkus.hibernate.orm.panache.PanacheRepositoryBase"); 219 | if (panacheRepositoryType == null) 220 | return null; 221 | if (types.isSubtype(type, types.erasure(panacheRepositoryType.asType()))) { 222 | TypeMirror ret = getFirstTypeArg(types, element.asType(), panacheRepositoryType); 223 | return ret != null ? (TypeElement)types.asElement(ret) : null; 224 | } 225 | } 226 | return null; 227 | } 228 | 229 | private static TypeMirror getFirstTypeArg(Types types, TypeMirror type, TypeElement superType) { 230 | // look at this first 231 | TypeElement typeElement = ((TypeElement)types.asElement(type)); 232 | if (typeElement.getQualifiedName().contentEquals(superType.getQualifiedName())) { 233 | return superType.getTypeParameters().get(0).asType(); 234 | } 235 | // look up 236 | TypeMirror superclass = typeElement.getSuperclass(); 237 | if (superclass.getKind() != TypeKind.NONE) { 238 | // look at superclass 239 | TypeMirror ret = getFirstTypeArg(types, superclass, superType); 240 | if (ret != null) 241 | return mapGenerics(types, superclass, ret); 242 | } 243 | for (TypeMirror superInterface : typeElement.getInterfaces()) { 244 | TypeMirror ret = getFirstTypeArg(types, superInterface, superType); 245 | if (ret != null) 246 | return mapGenerics(types, superInterface, ret); 247 | } 248 | return null; 249 | } 250 | 251 | private static TypeMirror mapGenerics(Types types, TypeMirror superType, TypeMirror ret) { 252 | if (ret.getKind() == TypeKind.TYPEVAR) { 253 | TypeElement superElement = (TypeElement) types.asElement(superType); 254 | int typeParamIndex = superElement.getTypeParameters().indexOf(types.asElement(ret)); 255 | DeclaredType superDeclaredType = (DeclaredType) superType; 256 | List superTypeArgs = superDeclaredType.getTypeArguments(); 257 | if (typeParamIndex != -1 && typeParamIndex < superTypeArgs.size()) { 258 | ret = superTypeArgs.get(typeParamIndex); 259 | return ret; 260 | } 261 | // not really found 262 | return null; 263 | } 264 | return ret; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/Parent.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import java.io.OutputStream; 4 | 5 | @SuppressWarnings("all") 6 | public class Parent { 7 | boolean first; 8 | static final Object staticObj = OutputStream.class; 9 | volatile Object second; 10 | private static volatile boolean staticSecond; 11 | private static volatile boolean staticThird; 12 | } -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/Permit.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2021 The Project Lombok Authors. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | package org.hibernate.query.validator; 23 | 24 | import java.lang.reflect.AccessibleObject; 25 | import java.lang.reflect.Constructor; 26 | import java.lang.reflect.Field; 27 | import java.lang.reflect.InvocationTargetException; 28 | import java.lang.reflect.Method; 29 | 30 | /** 31 | * COPY/PASTE from Lombok! 32 | */ 33 | // sunapi suppresses javac's warning about using Unsafe; 'all' suppresses 34 | // eclipse's warning about the unspecified 'sunapi' key. Leave them both. 35 | // Yes, javac's definition of the word 'all' is quite contrary to what the 36 | // dictionary says it means. 'all' does NOT include 'sunapi' according to javac. 37 | @SuppressWarnings({"sunapi", "all"}) 38 | public class Permit { 39 | private Permit() {} 40 | 41 | 42 | private static final long ACCESSIBLE_OVERRIDE_FIELD_OFFSET; 43 | private static final IllegalAccessException INIT_ERROR; 44 | private static final sun.misc.Unsafe UNSAFE = (sun.misc.Unsafe) reflectiveStaticFieldAccess(sun.misc.Unsafe.class, "theUnsafe"); 45 | 46 | static { 47 | Field f; 48 | long g; 49 | Throwable ex; 50 | 51 | try { 52 | g = getOverrideFieldOffset(); 53 | ex = null; 54 | } catch (Throwable t) { 55 | f = null; 56 | g = -1L; 57 | ex = t; 58 | } 59 | 60 | ACCESSIBLE_OVERRIDE_FIELD_OFFSET = g; 61 | if (ex == null) INIT_ERROR = null; 62 | else if (ex instanceof IllegalAccessException) INIT_ERROR = (IllegalAccessException) ex; 63 | else { 64 | INIT_ERROR = new IllegalAccessException("Cannot initialize Unsafe-based permit"); 65 | INIT_ERROR.initCause(ex); 66 | } 67 | } 68 | 69 | public static T setAccessible(T accessor) { 70 | if (INIT_ERROR == null) { 71 | UNSAFE.putBoolean(accessor, ACCESSIBLE_OVERRIDE_FIELD_OFFSET, true); 72 | } else { 73 | accessor.setAccessible(true); 74 | } 75 | 76 | return accessor; 77 | } 78 | 79 | private static long getOverrideFieldOffset() throws Throwable { 80 | Field f = null; 81 | Throwable saved = null; 82 | try { 83 | f = AccessibleObject.class.getDeclaredField("override"); 84 | } catch (Throwable t) { 85 | saved = t; 86 | } 87 | 88 | if (f != null) { 89 | return UNSAFE.objectFieldOffset(f); 90 | } 91 | // The below seems very risky, but for all AccessibleObjects in java today it does work, 92 | // and starting with JDK12, making the field accessible is no longer possible. 93 | try { 94 | return UNSAFE.objectFieldOffset(Fake.class.getDeclaredField("override")); 95 | } catch (Throwable t) { 96 | throw saved; 97 | } 98 | } 99 | 100 | static class Fake { 101 | boolean override; 102 | Object accessCheckCache; 103 | } 104 | 105 | public static Method getMethod(Class c, String mName, Class... parameterTypes) throws NoSuchMethodException { 106 | Method m = null; 107 | Class oc = c; 108 | while (c != null) { 109 | try { 110 | m = c.getDeclaredMethod(mName, parameterTypes); 111 | break; 112 | } catch (NoSuchMethodException e) {} 113 | c = c.getSuperclass(); 114 | } 115 | 116 | if (m == null) throw new NoSuchMethodException(oc.getName() + " :: " + mName + "(args)"); 117 | return setAccessible(m); 118 | } 119 | 120 | public static Method permissiveGetMethod(Class c, String mName, Class... parameterTypes) { 121 | try { 122 | return getMethod(c, mName, parameterTypes); 123 | } catch (Exception ignore) { 124 | return null; 125 | } 126 | } 127 | 128 | public static Field getField(Class c, String fName) throws NoSuchFieldException { 129 | Field f = null; 130 | Class oc = c; 131 | while (c != null) { 132 | try { 133 | f = c.getDeclaredField(fName); 134 | break; 135 | } catch (NoSuchFieldException e) {} 136 | c = c.getSuperclass(); 137 | } 138 | 139 | if (f == null) throw new NoSuchFieldException(oc.getName() + " :: " + fName); 140 | 141 | return setAccessible(f); 142 | } 143 | 144 | public static Field permissiveGetField(Class c, String fName) { 145 | try { 146 | return getField(c, fName); 147 | } catch (Exception ignore) { 148 | return null; 149 | } 150 | } 151 | 152 | public static T permissiveReadField(Class type, Field f, Object instance) { 153 | try { 154 | return type.cast(f.get(instance)); 155 | } catch (Exception ignore) { 156 | return null; 157 | } 158 | } 159 | 160 | public static Constructor getConstructor(Class c, Class... parameterTypes) 161 | throws NoSuchMethodException { 162 | return setAccessible(c.getDeclaredConstructor(parameterTypes)); 163 | } 164 | 165 | private static Object reflectiveStaticFieldAccess(Class c, String fName) { 166 | try { 167 | Field f = c.getDeclaredField(fName); 168 | f.setAccessible(true); 169 | return f.get(null); 170 | } catch (Exception e) { 171 | return null; 172 | } 173 | } 174 | 175 | public static boolean isDebugReflection() { 176 | return !"false".equals(System.getProperty("lombok.debug.reflection", "false")); 177 | } 178 | 179 | public static void handleReflectionDebug(Throwable t, Throwable initError) { 180 | if (!isDebugReflection()) return; 181 | 182 | System.err.println("** LOMBOK REFLECTION exception: " + t.getClass() + ": " + 183 | (t.getMessage() == null ? "(no message)" : t.getMessage())); 184 | t.printStackTrace(System.err); 185 | if (initError != null) { 186 | System.err.println("*** ADDITIONALLY, exception occurred setting up reflection: "); 187 | initError.printStackTrace(System.err); 188 | } 189 | } 190 | 191 | public static Object invoke(Method m, Object receiver, Object... args) 192 | throws IllegalAccessException, InvocationTargetException { 193 | return invoke(null, m, receiver, args); 194 | } 195 | 196 | public static Object invoke(Throwable initError, Method m, Object receiver, Object... args) 197 | throws IllegalAccessException, InvocationTargetException { 198 | try { 199 | return m.invoke(receiver, args); 200 | } catch (IllegalAccessException e) { 201 | handleReflectionDebug(e, initError); 202 | throw e; 203 | } catch (RuntimeException e) { 204 | handleReflectionDebug(e, initError); 205 | throw e; 206 | } catch (Error e) { 207 | handleReflectionDebug(e, initError); 208 | throw e; 209 | } 210 | } 211 | 212 | public static Object invokeSneaky(Method m, Object receiver, Object... args) { 213 | return invokeSneaky(null, m, receiver, args); 214 | } 215 | 216 | public static Object invokeSneaky(Throwable initError, Method m, Object receiver, Object... args) { 217 | try { 218 | return m.invoke(receiver, args); 219 | } catch (NoClassDefFoundError e) { 220 | handleReflectionDebug(e, initError); 221 | //ignore, we don't have access to the correct ECJ classes, so lombok can't possibly 222 | //do anything useful here. 223 | return null; 224 | } catch (NullPointerException e) { 225 | handleReflectionDebug(e, initError); 226 | //ignore, we don't have access to the correct ECJ classes, so lombok can't possibly 227 | //do anything useful here. 228 | return null; 229 | } catch (IllegalAccessException e) { 230 | handleReflectionDebug(e, initError); 231 | throw sneakyThrow(e); 232 | } catch (InvocationTargetException e) { 233 | throw sneakyThrow(e.getCause()); 234 | } catch (RuntimeException e) { 235 | handleReflectionDebug(e, initError); 236 | throw e; 237 | } catch (Error e) { 238 | handleReflectionDebug(e, initError); 239 | throw e; 240 | } 241 | } 242 | 243 | public static T newInstance(Constructor c, Object... args) 244 | throws IllegalAccessException, InvocationTargetException, InstantiationException { 245 | return newInstance(null, c, args); 246 | } 247 | 248 | public static T newInstance(Throwable initError, Constructor c, Object... args) 249 | throws IllegalAccessException, InvocationTargetException, InstantiationException { 250 | try { 251 | return c.newInstance(args); 252 | } catch (IllegalAccessException e) { 253 | handleReflectionDebug(e, initError); 254 | throw e; 255 | } catch (InstantiationException e) { 256 | handleReflectionDebug(e, initError); 257 | throw e; 258 | } catch (RuntimeException e) { 259 | handleReflectionDebug(e, initError); 260 | throw e; 261 | } catch (Error e) { 262 | handleReflectionDebug(e, initError); 263 | throw e; 264 | } 265 | } 266 | 267 | public static T newInstanceSneaky(Constructor c, Object... args) { 268 | return newInstanceSneaky(null, c, args); 269 | } 270 | 271 | public static T newInstanceSneaky(Throwable initError, Constructor c, Object... args) { 272 | try { 273 | return c.newInstance(args); 274 | } catch (NoClassDefFoundError e) { 275 | handleReflectionDebug(e, initError); 276 | //ignore, we don't have access to the correct ECJ classes, so lombok can't possibly 277 | //do anything useful here. 278 | return null; 279 | } catch (NullPointerException e) { 280 | handleReflectionDebug(e, initError); 281 | //ignore, we don't have access to the correct ECJ classes, so lombok can't possibly 282 | //do anything useful here. 283 | return null; 284 | } catch (IllegalAccessException e) { 285 | handleReflectionDebug(e, initError); 286 | throw sneakyThrow(e); 287 | } catch (InstantiationException e) { 288 | handleReflectionDebug(e, initError); 289 | throw sneakyThrow(e); 290 | } catch (InvocationTargetException e) { 291 | throw sneakyThrow(e.getCause()); 292 | } catch (RuntimeException e) { 293 | handleReflectionDebug(e, initError); 294 | throw e; 295 | } catch (Error e) { 296 | handleReflectionDebug(e, initError); 297 | throw e; 298 | } 299 | } 300 | 301 | public static Object get(Field f, Object receiver) throws IllegalAccessException { 302 | try { 303 | return f.get(receiver); 304 | } catch (IllegalAccessException e) { 305 | handleReflectionDebug(e, null); 306 | throw e; 307 | } catch (RuntimeException e) { 308 | handleReflectionDebug(e, null); 309 | throw e; 310 | } catch (Error e) { 311 | handleReflectionDebug(e, null); 312 | throw e; 313 | } 314 | } 315 | 316 | public static void set(Field f, Object receiver, Object newValue) throws IllegalAccessException { 317 | try { 318 | f.set(receiver, newValue); 319 | } catch (IllegalAccessException e) { 320 | handleReflectionDebug(e, null); 321 | throw e; 322 | } catch (RuntimeException e) { 323 | handleReflectionDebug(e, null); 324 | throw e; 325 | } catch (Error e) { 326 | handleReflectionDebug(e, null); 327 | throw e; 328 | } 329 | } 330 | 331 | public static void reportReflectionProblem(Throwable initError, String msg) { 332 | if (!isDebugReflection()) return; 333 | System.err.println("** LOMBOK REFLECTION issue: " + msg); 334 | if (initError != null) { 335 | System.err.println("*** ADDITIONALLY, exception occurred setting up reflection: "); 336 | initError.printStackTrace(System.err); 337 | } 338 | } 339 | 340 | public static RuntimeException sneakyThrow(Throwable t) { 341 | if (t == null) throw new NullPointerException("t"); 342 | return Permit.sneakyThrow0(t); 343 | } 344 | 345 | @SuppressWarnings("unchecked") 346 | private static T sneakyThrow0(Throwable t) throws T { 347 | throw (T)t; 348 | } 349 | 350 | } -------------------------------------------------------------------------------- /src/main/java/org/hibernate/query/validator/Validation.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator; 2 | 3 | import org.antlr.v4.runtime.ANTLRErrorListener; 4 | import org.antlr.v4.runtime.BailErrorStrategy; 5 | import org.antlr.v4.runtime.DefaultErrorStrategy; 6 | import org.antlr.v4.runtime.Token; 7 | import org.antlr.v4.runtime.atn.PredictionMode; 8 | import org.antlr.v4.runtime.misc.ParseCancellationException; 9 | import org.hibernate.PropertyNotFoundException; 10 | import org.hibernate.QueryException; 11 | import org.hibernate.grammars.hql.HqlLexer; 12 | import org.hibernate.grammars.hql.HqlParser; 13 | import org.hibernate.query.hql.internal.HqlParseTreeBuilder; 14 | import org.hibernate.query.hql.internal.SemanticQueryBuilder; 15 | import org.hibernate.query.sqm.EntityTypeException; 16 | import org.hibernate.query.sqm.PathElementException; 17 | import org.hibernate.query.sqm.TerminalPathException; 18 | import org.hibernate.type.descriptor.java.spi.JdbcTypeRecommendationException; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.Set; 23 | 24 | import static java.lang.Character.isJavaIdentifierStart; 25 | import static java.lang.Integer.parseInt; 26 | import static java.util.stream.Stream.concat; 27 | 28 | /** 29 | * @author Gavin King 30 | */ 31 | class Validation { 32 | 33 | interface Handler extends ANTLRErrorListener { 34 | void error(int start, int end, String message); 35 | void warn(int start, int end, String message); 36 | 37 | int getErrorCount(); 38 | } 39 | 40 | static void validate(String hql, boolean checkParams, 41 | Set setParameterLabels, 42 | Set setParameterNames, 43 | Handler handler, 44 | MockSessionFactory factory) { 45 | validate(hql, checkParams, setParameterLabels, setParameterNames, handler, factory, 0); 46 | } 47 | 48 | static void validate(String hql, boolean checkParams, 49 | Set setParameterLabels, 50 | Set setParameterNames, 51 | Handler handler, 52 | MockSessionFactory factory, 53 | int errorOffset) { 54 | // handler = new Filter(handler, errorOffset); 55 | 56 | try { 57 | 58 | final HqlLexer hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( hql ); 59 | final HqlParser hqlParser = HqlParseTreeBuilder.INSTANCE.buildHqlParser( hql, hqlLexer ); 60 | hqlLexer.addErrorListener( handler ); 61 | hqlParser.getInterpreter().setPredictionMode( PredictionMode.SLL ); 62 | hqlParser.removeErrorListeners(); 63 | hqlParser.addErrorListener( handler ); 64 | hqlParser.setErrorHandler( new BailErrorStrategy() ); 65 | 66 | HqlParser.StatementContext statementContext; 67 | try { 68 | statementContext = hqlParser.statement(); 69 | } 70 | catch ( ParseCancellationException e) { 71 | // reset the input token stream and parser state 72 | hqlLexer.reset(); 73 | hqlParser.reset(); 74 | 75 | // fall back to LL(k)-based parsing 76 | hqlParser.getInterpreter().setPredictionMode( PredictionMode.LL ); 77 | hqlParser.setErrorHandler( new DefaultErrorStrategy() ); 78 | 79 | statementContext = hqlParser.statement(); 80 | 81 | } 82 | if (handler.getErrorCount() == 0) { 83 | try { 84 | new SemanticQueryBuilder<>( Object[].class, () -> false, factory ) 85 | .visitStatement( statementContext ); 86 | } 87 | catch (JdbcTypeRecommendationException ignored) { 88 | // just squash these for now 89 | } 90 | catch (QueryException | PathElementException | TerminalPathException | EntityTypeException 91 | | PropertyNotFoundException se) { //TODO is this one really thrown by core? It should not be! 92 | handler.error( -errorOffset+1, -errorOffset + hql.length(), se.getMessage() ); 93 | } 94 | } 95 | 96 | if (checkParams) { 97 | checkParameterBinding(hql, setParameterLabels, setParameterNames, handler, errorOffset); 98 | } 99 | } 100 | catch (Exception e) { 101 | e.printStackTrace(); 102 | } 103 | } 104 | 105 | private static void checkParameterBinding( 106 | String hql, 107 | Set setParameterLabels, 108 | Set setParameterNames, 109 | Handler handler, 110 | int errorOffset) { 111 | try { 112 | String unsetParams = null; 113 | String notSet = null; 114 | String parameters = null; 115 | int start = -1; 116 | int end = -1; 117 | List names = new ArrayList<>(); 118 | List labels = new ArrayList<>(); 119 | final HqlLexer hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( hql ); 120 | loop: 121 | while (true) { 122 | Token token = hqlLexer.nextToken(); 123 | int tokenType = token.getType(); 124 | switch (tokenType) { 125 | case HqlLexer.EOF: 126 | break loop; 127 | case HqlLexer.QUESTION_MARK: 128 | case HqlLexer.COLON: 129 | Token next = hqlLexer.nextToken(); 130 | String text = next.getText(); 131 | switch (tokenType) { 132 | case HqlLexer.COLON: 133 | if (!text.isEmpty() 134 | && isJavaIdentifierStart(text.codePointAt(0))) { 135 | names.add(text); 136 | if (setParameterNames.contains(text)) { 137 | continue; 138 | } 139 | } 140 | else { 141 | continue; 142 | } 143 | break; 144 | case HqlLexer.QUESTION_MARK: 145 | if (next.getType() == HqlLexer.INTEGER_LITERAL) { 146 | int label; 147 | try { 148 | label = parseInt(text); 149 | } 150 | catch (NumberFormatException nfe) { 151 | continue; 152 | } 153 | labels.add(label); 154 | if (setParameterLabels.contains(label)) { 155 | continue; 156 | } 157 | } 158 | else { 159 | continue; 160 | } 161 | break; 162 | default: 163 | continue; 164 | } 165 | parameters = unsetParams == null ? "Parameter " : "Parameters "; 166 | notSet = unsetParams == null ? " is not set" : " are not set"; 167 | unsetParams = unsetParams == null ? "" : unsetParams + ", "; 168 | unsetParams += token.getText() + text; 169 | if (start == -1) 170 | start = token.getCharPositionInLine(); //TODO: wrong for multiline query strings! 171 | end = token.getCharPositionInLine() + text.length() + 1; 172 | break; 173 | } 174 | } 175 | if (unsetParams != null) { 176 | handler.warn(start-errorOffset+1, end-errorOffset, parameters + unsetParams + notSet); 177 | } 178 | 179 | setParameterNames.removeAll(names); 180 | setParameterLabels.removeAll(labels); 181 | 182 | int count = setParameterNames.size() + setParameterLabels.size(); 183 | if (count > 0) { 184 | String missingParams = 185 | concat(setParameterNames.stream().map(name -> ":" + name), 186 | setParameterLabels.stream().map(label -> "?" + label)) 187 | .reduce((x, y) -> x + ", " + y) 188 | .orElse(null); 189 | String params = 190 | count == 1 ? 191 | "Parameter " : 192 | "Parameters "; 193 | String notOccur = 194 | count == 1 ? 195 | " does not occur in the query" : 196 | " do not occur in the query"; 197 | handler.warn(0, 0, params + missingParams + notOccur); 198 | } 199 | } 200 | finally { 201 | setParameterNames.clear(); 202 | setParameterLabels.clear(); 203 | } 204 | } 205 | 206 | 207 | // private static class Filter implements Handler { 208 | // private final Handler delegate; 209 | // private final int errorOffset; 210 | // private int errorCount; 211 | // 212 | // @Override 213 | // public int getErrorCount() { 214 | // return errorCount; 215 | // } 216 | // 217 | // private Filter(Handler delegate, int errorOffset) { 218 | // this.delegate = delegate; 219 | // this.errorOffset = errorOffset; 220 | // } 221 | // 222 | // @Override 223 | // public void error(int start, int end, String message) { 224 | // delegate.error(start - errorOffset, end - errorOffset, message); 225 | // } 226 | // 227 | // @Override 228 | // public void warn(int start, int end, String message) { 229 | // delegate.warn(start - errorOffset, end - errorOffset, message); 230 | // } 231 | // 232 | // @Override 233 | // public void syntaxError(Recognizer recognizer, 234 | // Object offendingSymbol, 235 | // int line, int charPositionInLine, 236 | // String msg, 237 | // RecognitionException e) { 238 | //// if (errorCount > 0 && e instanceof NoViableAltException) { 239 | //// //ignore it, it's probably a useless "unexpected end of subtree" 240 | //// return; 241 | //// } 242 | // errorCount++; 243 | // delegate.syntaxError(recognizer, offendingSymbol, line, charPositionInLine, msg, e); 244 | // } 245 | // 246 | // @Override 247 | // public void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean b, BitSet bitSet, ATNConfigSet atnConfigSet) { 248 | // } 249 | // 250 | // @Override 251 | // public void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitSet, ATNConfigSet atnConfigSet) { 252 | // } 253 | // 254 | // @Override 255 | // public void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atnConfigSet) { 256 | // } 257 | // } 258 | } 259 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | org.hibernate.query.validator.HQLProcessor -------------------------------------------------------------------------------- /src/test/java/org/hibernate/query/validator/test/HQLValidationTest.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.query.validator.test; 2 | 3 | import org.eclipse.jdt.core.compiler.batch.BatchCompiler; 4 | import org.junit.Test; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.io.PrintWriter; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import static javax.tools.ToolProvider.getSystemJavaCompiler; 16 | import static org.hibernate.query.validator.HQLProcessor.forceEclipseForTesting; 17 | import static org.junit.Assert.assertFalse; 18 | import static org.junit.Assert.assertTrue; 19 | 20 | public class HQLValidationTest { 21 | 22 | private final Path TEST_LIBS = Paths.get("test-runtime-libs"); 23 | 24 | @Test 25 | public void testJavac() throws Exception { 26 | String errors = compileWithJavac("test", "test.test"); 27 | 28 | assertFalse(errors.contains("GoodQueries.java:")); 29 | assertFalse(errors.contains("PanachePerson.java:")); 30 | assertFalse(errors.contains("PanacheRepository.java:")); 31 | 32 | assertTrue(errors.contains("BadQueries.java:9: error: no viable alternative at input '*do'")); 33 | assertTrue(errors.contains("BadQueries.java:10: error: no viable alternative at input 'from")); 34 | assertTrue(errors.contains("BadQueries.java:11: error: no viable alternative at input '")); 35 | assertTrue(errors.contains("BadQueries.java:12: error: no viable alternative at input '")); 36 | assertTrue(errors.contains("BadQueries.java:13: error: Could not interpret path expression 'from")); 37 | 38 | assertTrue(errors.contains("BadQueries.java:15: error: Could not resolve class 'test.Nil' named for instantiation")); 39 | // assertTrue(errors.contains("BadQueries.java:16: error: test.Pair has no suitable constructor for types (Person)")); 40 | // assertTrue(errors.contains("BadQueries.java:17: error: test.Pair has no suitable constructor for types (Person, string)")); 41 | // assertTrue(errors.contains("BadQueries.java:53: error: test.Pair has no suitable constructor for types (integer, integer)")); 42 | // assertTrue(errors.contains("BadQueries.java:54: error: test.Pair has no suitable constructor for types (string, string)")); 43 | 44 | assertTrue(errors.contains("BadQueries.java:19: error: Could not resolve root entity 'People'")); 45 | assertTrue(errors.contains("BadQueries.java:20: error: Could not resolve attribute 'firstName' of 'Person'")); 46 | assertTrue(errors.contains("BadQueries.java:21: error: Could not resolve attribute 'addr' of 'Person'")); 47 | assertTrue(errors.contains("BadQueries.java:22: error: Could not resolve attribute 'town' of 'Address'")); 48 | assertTrue(errors.contains("BadQueries.java:23: error: Could not resolve attribute 'name' of 'Address'")); 49 | assertTrue(errors.contains("BadQueries.java:24: error: Could not resolve attribute 'type' of 'Country'")); 50 | 51 | assertTrue(errors.contains("BadQueries.java:26: error: Could not interpret attribute 'length' of basic-valued path")); 52 | assertTrue(errors.contains("BadQueries.java:27: error: Terminal path has no attribute 'length'")); 53 | 54 | assertTrue(errors.contains("BadQueries.java:29: error: Could not interpret path expression 'xxx'")); 55 | // assertTrue(errors.contains("BadQueries.java:30: warning: func is not defined")); 56 | // assertTrue(errors.contains("BadQueries.java:31: warning: custom is not defined")); 57 | // assertTrue(errors.contains("BadQueries.java:32: warning: p is not defined")); 58 | assertTrue(errors.contains("BadQueries.java:32: error: Could not interpret path expression 'p.name'")); 59 | 60 | //TODO re-enable! 61 | // assertTrue(errors.contains("BadQueries.java:34: error: key(), value(), or entry() argument must be map element")); 62 | // assertTrue(errors.contains("BadQueries.java:35: error: key(), value(), or entry() argument must be map element")); 63 | // assertTrue(errors.contains("BadQueries.java:36: error: key(), value(), or entry() argument must be map element")); 64 | 65 | assertTrue(errors.contains("BadQueries.java:39: error: mismatched input '.', expecting")); 66 | 67 | assertTrue(errors.contains("BadQueries.java:41: error: Unlabeled ordinal parameter ('?' rather than ?1)")); 68 | 69 | assertTrue(errors.contains("Person.java:22: error: Could not resolve attribute 'x' of 'Person'")); 70 | 71 | assertTrue(errors.contains("BadQueries.java:46: warning: Parameter ?2 is not set")); 72 | assertTrue(errors.contains("BadQueries.java:48: warning: Parameter :name is not set")); 73 | 74 | assertTrue(errors.contains("BadQueries.java:51: warning: Parameter :hello does not occur in the query")); 75 | 76 | assertTrue(errors.contains("BadQueries.java:56: error: token recognition error at: ''gavin'")); 77 | assertTrue(errors.contains("BadQueries.java:57: error: token recognition error at: ''gavin'")); 78 | 79 | assertTrue(errors.contains("BadQueries.java:59: error: no viable alternative at input '*fromPerson'")); 80 | 81 | assertTrue(errors.contains("BadQueries.java:62: error: Could not resolve treat target type 'Employe'")); 82 | 83 | assertPanacheErrors(errors, "PanacheBadPerson", 22); 84 | assertPanacheErrors(errors, "PanacheBadPersonRepository", 10); 85 | } 86 | 87 | private void assertPanacheErrors(String errors, String name, int start) { 88 | // assertTrue(errors.contains(name+".java:"+(start)+": warning: missing is not defined (add it to whitelist)")); 89 | assertTrue(errors.contains(name+".java:"+(start+1)+": warning: Parameter ?2 is not set")); 90 | assertTrue(errors.contains(name+".java:"+(start+2)+": warning: Parameter :id is not set")); 91 | assertTrue(errors.contains(name+".java:"+(start+3)+": warning: Missing required argument for 'name'")); 92 | assertTrue(errors.contains(name+".java:"+(start+4)+": warning: Parameter :bar is not set")); 93 | assertTrue(errors.contains(name+".java:"+(start+5)+": warning: Missing required argument for 'name'")); 94 | assertTrue(errors.contains(name+".java:"+(start+6)+": warning: Missing required argument for 'name'")); 95 | assertTrue(errors.contains(name+".java:"+(start+7)+": warning: Missing required argument for 'name'")); 96 | assertTrue(errors.contains(name+".java:"+(start+8)+": warning: Missing required argument for 'name'")); 97 | assertTrue(errors.contains(name+".java:"+(start+9)+": warning: Missing required argument for 'name'")); 98 | assertTrue(errors.contains(name+".java:"+(start+10)+": warning: Missing required argument for 'name'")); 99 | assertTrue(errors.contains(name+".java:"+(start+11)+": warning: Missing required argument for 'name'")); 100 | assertTrue(errors.contains(name+".java:"+(start+12)+": warning: Too many arguments for 'name'")); 101 | } 102 | 103 | @Test 104 | public void testECJ() throws Exception { 105 | String errors = compileWithECJ("test", "test.test"); 106 | 107 | assertECJ(errors); 108 | } 109 | 110 | private void assertECJ(String errors) { 111 | assertFalse(errors.contains("GoodQueries.java")); 112 | assertFalse(errors.contains("PanachePerson.java")); 113 | assertFalse(errors.contains("PanachePersonRepository.java")); 114 | 115 | assertTrue(errors.contains("mismatched input 'do'") && errors.contains("BadQueries.java (at line 9)")); 116 | assertTrue(errors.contains("no viable alternative at input 'from") && errors.contains("BadQueries.java (at line 10)")); 117 | assertTrue(errors.contains("no viable alternative at input '") && errors.contains("BadQueries.java (at line 11)")); 118 | assertTrue(errors.contains("no viable alternative at input '") && errors.contains("BadQueries.java (at line 12)")); 119 | assertTrue(errors.contains("Could not interpret path expression 'from") && errors.contains("BadQueries.java (at line 13)")); 120 | 121 | assertTrue(errors.contains("Could not resolve class 'test.Nil' named for instantiation")); 122 | // assertTrue(errors.contains("test.Pair has no suitable constructor for types (Person)")); 123 | // assertTrue(errors.contains("test.Pair has no suitable constructor for types (Person, string)")); 124 | // assertTrue(errors.contains("test.Pair has no suitable constructor for types (string, string)")); 125 | // assertTrue(errors.contains("test.Pair has no suitable constructor for types (integer, integer)")); 126 | 127 | assertTrue(errors.contains("Could not resolve root entity 'People'") && errors.contains("BadQueries.java (at line 19)")); 128 | assertTrue(errors.contains("Could not resolve attribute 'firstName' of 'Person'") && errors.contains("BadQueries.java (at line 20)")); 129 | assertTrue(errors.contains("Could not resolve attribute 'addr' of 'Person'") && errors.contains("BadQueries.java (at line 21)")); 130 | assertTrue(errors.contains("Could not resolve attribute 'town' of 'Address'") && errors.contains("BadQueries.java (at line 22)")); 131 | assertTrue(errors.contains("Could not resolve attribute 'name' of 'Address'") && errors.contains("BadQueries.java (at line 13)")); 132 | assertTrue(errors.contains("Could not resolve attribute 'type' of 'Country'") && errors.contains("BadQueries.java (at line 24)")); 133 | 134 | assertTrue(errors.contains("Could not interpret attribute 'length' of basic-valued path") && errors.contains("BadQueries.java (at line 26)")); 135 | assertTrue(errors.contains("Terminal path has no attribute 'length'") && errors.contains("BadQueries.java (at line 27)")); 136 | 137 | assertTrue(errors.contains("Could not interpret path expression 'xxx'") && errors.contains("BadQueries.java (at line 29)")); 138 | // assertTrue(errors.contains("func is not defined") && errors.contains("BadQueries.java (at line 30)")); 139 | // assertTrue(errors.contains("custom is not defined") && errors.contains("BadQueries.java (at line 31)")); 140 | // assertTrue(errors.contains("p is not defined") && errors.contains("BadQueries.java (at line 32)")); 141 | assertTrue(errors.contains("Could not interpret path expression 'p.name'") && errors.contains("BadQueries.java (at line 32)")); 142 | 143 | assertTrue(errors.contains("mismatched input '.', expecting") && errors.contains("BadQueries.java (at line 39)")); 144 | 145 | assertTrue(errors.contains("Unlabeled ordinal parameter ('?' rather than ?1)") && errors.contains("BadQueries.java (at line 41)")); 146 | 147 | assertTrue(errors.contains("Could not resolve attribute 'x' of 'Person'") && errors.contains("Person.java (at line 22)")); 148 | 149 | assertTrue(errors.contains("Parameter ?2 is not set") && errors.contains("BadQueries.java (at line 46)")); 150 | assertTrue(errors.contains("Parameter :name is not set") && errors.contains("BadQueries.java (at line 48)")); 151 | 152 | assertTrue(errors.contains("Parameter :hello does not occur in the query") && errors.contains("BadQueries.java (at line 51)")); 153 | 154 | assertTrue(errors.contains("BadQueries.java (at line 46)") && errors.contains("Parameter ?2 is not set")); 155 | assertTrue(errors.contains("BadQueries.java (at line 48)") && errors.contains("Parameter :name is not set")); 156 | 157 | assertTrue(errors.contains("BadQueries.java (at line 51)") && errors.contains(":hello does not occur in the query")); 158 | 159 | assertTrue(errors.contains("no viable alternative at input '") && errors.contains("token recognition error at: ''gavin'") 160 | && errors.contains("BadQueries.java (at line 56)")); 161 | assertTrue(errors.contains("no viable alternative at input '") && errors.contains("token recognition error at: ''gavin'") 162 | && errors.contains("BadQueries.java (at line 57)")); 163 | 164 | assertTrue(errors.contains("no viable alternative at input '*fromPerson'") && errors.contains("BadQueries.java (at line 59)")); 165 | 166 | assertTrue(errors.contains("Could not resolve treat target type 'Employe'") && errors.contains("BadQueries.java (at line 62)")); 167 | 168 | assertPanacheErrorsEcj(errors, "PanacheBadPerson", 22); 169 | assertPanacheErrorsEcj(errors, "PanacheBadPersonRepository", 10); 170 | } 171 | 172 | private void assertPanacheErrorsEcj(String errors, String name, int start) { 173 | // assertTrue(errors.contains(name+".java (at line "+(start)+")") && errors.contains("Could not interpret path expression 'missing'")); 174 | assertTrue(errors.contains(name+".java (at line "+(start+1)+")") && errors.contains("Parameter ?2 is not set")); 175 | assertTrue(errors.contains(name+".java (at line "+(start+2)+")") && errors.contains("Parameter :id is not set")); 176 | assertTrue(errors.contains(name+".java (at line "+(start+3)+")") && errors.contains("Missing required argument for 'name'")); 177 | assertTrue(errors.contains(name+".java (at line "+(start+4)+")") && errors.contains("Parameter :bar is not set")); 178 | assertTrue(errors.contains(name+".java (at line "+(start+5)+")") && errors.contains("Missing required argument for 'name'")); 179 | assertTrue(errors.contains(name+".java (at line "+(start+6)+")") && errors.contains("Missing required argument for 'name'")); 180 | assertTrue(errors.contains(name+".java (at line "+(start+7)+")") && errors.contains("Missing required argument for 'name'")); 181 | assertTrue(errors.contains(name+".java (at line "+(start+8)+")") && errors.contains("Missing required argument for 'name'")); 182 | assertTrue(errors.contains(name+".java (at line "+(start+9)+")") && errors.contains("Missing required argument for 'name'")); 183 | assertTrue(errors.contains(name+".java (at line "+(start+10)+")") && errors.contains("Missing required argument for 'name'")); 184 | assertTrue(errors.contains(name+".java (at line "+(start+11)+")") && errors.contains("Missing required argument for 'name'")); 185 | assertTrue(errors.contains(name+".java (at line "+(start+12)+")") && errors.contains("Too many arguments for 'name'")); 186 | } 187 | 188 | @Test 189 | public void testEclipse() throws Exception { 190 | forceEclipseForTesting = true; 191 | String errors = compileWithECJ("test", "test.test"); 192 | 193 | assertECJ(errors); 194 | 195 | forceEclipseForTesting = false; 196 | } 197 | 198 | private String compileWithJavac(String... packages) throws IOException { 199 | Path tempDir = Files.createTempDirectory("validator-test-out"); 200 | 201 | List files = new ArrayList<>(); 202 | 203 | // files.add("-verbose"); 204 | 205 | files.add("-d"); 206 | files.add(tempDir.toString()); 207 | 208 | files.add("-classpath"); 209 | StringBuilder cp = new StringBuilder(); 210 | 211 | if (System.getProperty("gradle")!=null) { 212 | cp.append("build/libs/query-validator-2.0-SNAPSHOT.jar"); 213 | cp.append(":build/classes/java/main:build/classes/groovy/main"); 214 | } 215 | else { 216 | cp.append("out/production/query-validator"); 217 | cp.append(":build/classes/java/main"); 218 | } 219 | 220 | Files.list(TEST_LIBS) 221 | .map(Path::toString) 222 | .filter(s -> !s.contains("/ecj-") && ! s.contains("/org.eclipse.jdt.core_")) 223 | .forEach(s -> cp.append(":").append(s)); 224 | 225 | System.out.println(cp); 226 | files.add(cp.toString()); 227 | 228 | for (String pack: packages) { 229 | Files.list(Paths.get("src/test/source") 230 | .resolve(pack.replace('.', '/'))) 231 | .map(Path::toString) 232 | .filter(s -> s.endsWith(".java")) 233 | .forEach(files::add); 234 | } 235 | 236 | String[] args = files.toArray(new String[0]); 237 | ByteArrayOutputStream err = new ByteArrayOutputStream(); 238 | 239 | getSystemJavaCompiler().run(null, System.out, err, args); 240 | 241 | String errors = err.toString(); 242 | System.out.println(errors); 243 | return errors; 244 | } 245 | 246 | private String compileWithECJ(String... packages) throws IOException { 247 | Path tempDir = Files.createTempDirectory("validator-test-out"); 248 | 249 | List files = new ArrayList<>(); 250 | 251 | // files.add("-verbose"); 252 | 253 | files.add("-1.8"); 254 | 255 | files.add("-d"); 256 | files.add(tempDir.toString()); 257 | 258 | files.add("-classpath"); 259 | StringBuilder cp = new StringBuilder(); 260 | 261 | boolean useFatjar; 262 | if (System.getProperty("gradle")!=null) { 263 | useFatjar = forceEclipseForTesting 264 | && Files.exists(Paths.get("build/libs/query-validator-2.0-SNAPSHOT-all.jar")); 265 | if (useFatjar) { 266 | cp.append("build/libs/query-validator-2.0-SNAPSHOT-all.jar"); 267 | } 268 | else { 269 | cp.append("build/libs/query-validator-2.0-SNAPSHOT.jar"); 270 | cp.append(":build/classes/java/main:build/classes/groovy/main"); 271 | } 272 | } 273 | else { 274 | useFatjar = false; 275 | cp.append("build/classes/java/main:build/classes/groovy/main"); 276 | } 277 | 278 | Files.list(TEST_LIBS) 279 | .map(Path::toString) 280 | .filter(s -> useFatjar ? 281 | s.contains("/jakarta.persistence") 282 | || s.contains("/quarkus-") 283 | || s.contains("/hibernate-core") 284 | || s.contains("/org.eclipse.jdt.core_") : 285 | !s.contains(forceEclipseForTesting ? 286 | "/ecj-" : "/org.eclipse.jdt.core_")) 287 | 288 | .forEach(s -> cp.append(":").append(s)); 289 | 290 | System.out.println(cp); 291 | files.add(cp.toString()); 292 | 293 | for (String pack: packages) { 294 | Files.list(Paths.get("src/test/source") 295 | .resolve(pack.replace('.', '/'))) 296 | .map(Path::toString) 297 | .filter(s -> s.endsWith(".java")) 298 | .forEach(files::add); 299 | } 300 | 301 | String[] args = files.toArray(new String[0]); 302 | ByteArrayOutputStream err = new ByteArrayOutputStream(); 303 | 304 | BatchCompiler.compile(args, 305 | new PrintWriter(System.out), 306 | new PrintWriter(err), null); 307 | 308 | String errors = err.toString(); 309 | System.out.println(errors); 310 | return errors; 311 | } 312 | } -------------------------------------------------------------------------------- /src/test/source/test/Address.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import jakarta.persistence.Id; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.OneToMany; 6 | import java.util.Set; 7 | 8 | @Entity 9 | public class Address { 10 | @Id long id; 11 | public String street; 12 | public String city; 13 | public String zip; 14 | public Country country; 15 | @OneToMany(mappedBy = "address") 16 | public Set currentResidents; 17 | } 18 | -------------------------------------------------------------------------------- /src/test/source/test/BadQueries.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | //leave blank line or update test error locations 4 | //leave blank line or update test error locations 5 | 6 | public class BadQueries { 7 | 8 | public void badQueries() { 9 | createQuery("do"); //syntax error 10 | createQuery("from"); //syntax error 11 | createQuery("from Person where p.name='gavin' select"); //syntax error 12 | createQuery("from Person p where p.name+='gavin'"); //syntax error 13 | createQuery("select from Person where p.name='gavin'"); //syntax error 14 | if (1 == ".".length()) { 15 | createQuery("select new test.Nil(p,a) from Person p join p.address a"); //error 16 | createQuery("select new test.Pair(p) from Person p"); //error 17 | createQuery("select new test.Pair(p,p.name) from Person p"); //error 18 | } 19 | createQuery("from People p where p.name='gavin'"); //error 20 | createQuery("from Person p where p.firstName='gavin'"); //error 21 | createQuery("from Person p join p.addr a"); //error 22 | createQuery("from Person p where p.address.town='barcelona'"); //error 23 | createQuery("from Person p where p.name in (select a.name from Address a)"); //error 24 | createQuery("from Address add where add.country.type='au'"); //error 25 | for (int i=0; i<100; i++) { 26 | createQuery("select e from Employee e join e.contacts c where key(c).length > 0"); //error 27 | createQuery("select p.name, n from Person p join p.notes n where n.length>0"); //error 28 | } 29 | createQuery("select xxx from Person"); //warning 30 | createQuery("select func(p.name), year(current_date) from Person p"); //warning 31 | createQuery("from Person p where p.name = function('custom', p.id)"); //warning 32 | createQuery("select upper(p.name), year(current_date) from Person"); //error + warning 33 | 34 | createQuery("select p.name, n from Person p join p.notes n where key(n) = 0"); //error 35 | createQuery("select p.name, n from Person p join p.notes n where value(n) = ''"); //error 36 | createQuery("select p.name, n from Person p join p.notes n where entry(n) is not null"); //error 37 | createQuery("select p.name, n from Person p join p.notes n where index(n) = 0"); //error 38 | 39 | createQuery("select e from Employee e join e.contacts c where entry(c).value.address is null"); //error 40 | 41 | createQuery("from Person p where p.name = ? and p.id > ?"); //error 42 | 43 | createQuery("from Person p where max(indices(p.notes)) > 1"); //should be error! 44 | createQuery("from Person p where sum(elements(p.notes)) = ''"); //should be error! 45 | 46 | createQuery("from Person p where p.name = ?1 and p.id > ?2") 47 | .setParameter(1, "").getResultList(); //JPQL positional args 48 | createQuery("from Person p where p.name = :name and p.id >= :minId") 49 | .setParameter("minId", 12).getResultList(); //JPQL named args 50 | 51 | createQuery("from Person p").setParameter("hello", "world").getResultList(); 52 | 53 | createQuery("select new test.Pair(1,1) from Person p"); //"select new" with literals 54 | createQuery("select new test.Pair('','') from Person p"); //"select new" with literals 55 | 56 | createQuery("from Person p where p.name='gavin"); //error 57 | createQuery("from Person p where p.firstName='gavin"); //error 58 | 59 | createQuery("from" + "Person"); //error missing space in concatenated query 60 | createQuery("from " + "Person" + " order by 1"); 61 | 62 | createQuery("from Person p where treat(p.emergencyContact as Employe).employeeId = 2"); //JPQL "treat as" operator 63 | } 64 | 65 | private static Query createQuery(String s) { return new Query(); } 66 | private static class Query { 67 | public Query setParameter(String s, Object o) { return this; } 68 | public Query setParameter(int i, Object o) { return this; } 69 | public Query getResultList() { return this; } 70 | } 71 | } -------------------------------------------------------------------------------- /src/test/source/test/Country.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import jakarta.persistence.Embeddable; 4 | import jakarta.persistence.OneToMany; 5 | import java.util.Set; 6 | 7 | @Embeddable 8 | public class Country { 9 | public String code; 10 | public String name; 11 | public int thing = 0; 12 | @OneToMany 13 | public Set residents; 14 | } 15 | -------------------------------------------------------------------------------- /src/test/source/test/Email.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | @Embeddable 6 | public class Email { 7 | public String address; 8 | } 9 | -------------------------------------------------------------------------------- /src/test/source/test/Employee.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import java.util.Map; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Access; 6 | import jakarta.persistence.ManyToMany; 7 | 8 | import static jakarta.persistence.AccessType.PROPERTY; 9 | 10 | @Entity @Access(PROPERTY) 11 | public class Employee extends Person { 12 | private Integer id; 13 | private Map contacts; 14 | 15 | public Integer getEmployeeId() { return id; } 16 | 17 | @ManyToMany 18 | Map getContacts() { return contacts; } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/source/test/GoodQueries.java: -------------------------------------------------------------------------------- 1 | package test; 2 | import org.hibernate.annotations.processing.CheckHQL; 3 | 4 | @CheckHQL(dialect=org.hibernate.dialect.HSQLDialect.class) 5 | public class GoodQueries { 6 | 7 | public void goodQueries() { 8 | createQuery("from Person"); 9 | // createQuery("from " + "Person" + " order by 1"); 10 | createQuery("from test.Person"); //allowed by HQL 11 | createQuery("from Person p where lower(p.name)='gavin'"); 12 | createQuery("from Employee p where p.name='gavin' and p.employeeId=111"); 13 | createQuery("from Person p join p.address a where a.city='barcelona'"); 14 | createQuery("from Person p where p.address.city='barcelona'"); 15 | createQuery("from Person p join p.pastAddresses a where a.city='barcelona'"); 16 | 17 | createQuery("from Person p where p.name in (select p.name from Person p)"); //"in" operator with subquery 18 | createQuery("from Person p where exists (select a from p.pastAddresses a)"); //"exists" operator with correlated subquery 19 | 20 | createQuery("select new test.Pair(p,a) from Person p join p.address a"); //"select new" 21 | createQuery("select new test.Pair(p,p.address) from Person p join p.address a"); //"select new" 22 | createQuery("select new test.Pair('',1) from Person p"); //"select new" with literals 23 | 24 | createQuery("from Person p where size(p.pastAddresses) = 0"); //JPQL "size()" function 25 | createQuery("from Person p where exists elements(p.pastAddresses)"); //HQL "exists" operator with "elements()" function 26 | 27 | createQuery("from Person p where year(p.dob) > 1974"); //HQL "year()" function 28 | createQuery("select cast(p.dob as string) from Person p"); //HQL "cast()" function 29 | 30 | createQuery("select e, c, key(c) from Employee e join e.contacts c where key(c) = 'boss'"); //JPQL "key()" operator for Map 31 | createQuery("select e, entry(c) from Employee e join e.contacts c where c.address is null"); //JPQL "entry()" operator for Map 32 | createQuery("select e, value(c) from Employee e join e.contacts c where value(c).address is null"); //JPQL "value()" operator for Map 33 | createQuery("select distinct key(contact) from Employee e join e.contacts contact where length(key(contact))<10 and contact.address is not null and contact.address.city is not null"); 34 | 35 | createQuery("select substring(note,0,50) from Person p join p.notes note where index(note)<10"); //JPQL "index()" function for List 36 | createQuery("select substring(note,0,50) from Person p join p.notes note where length(note)>3"); //JPQL "length()" function for String 37 | createQuery("from Person p where size(p.notes) > 1"); //JPQL "size()" operator for collections 38 | 39 | createQuery("from Person p where '' = minelement(p.notes)"); //HQL "minelement()" function 40 | createQuery("from Person p where '' = maxelement(p.notes)"); //HQL "maxelement()" function 41 | createQuery("from Person p where maxindex(p.notes) > 1"); //HQL "maxindex()" function 42 | createQuery("from Person p where minindex(p.notes) > 1"); //HQL "minindex()" function 43 | createQuery("from Person p where 1 in indices(p.notes)"); //"in" operator with HQL "indices()" function 44 | createQuery("from Person p where '' in elements(p.notes)"); //"in" operator with HQL "elements()" function 45 | createQuery("from Person p where p.notes is empty"); //JPQL "is empty" operator 46 | createQuery("from Person p where p.notes is not empty"); //JPQL "is not empty" operator 47 | createQuery("select e from Employee e join e.contacts c where value(c).address is null"); 48 | 49 | createQuery("from Person p where type(p) = Employee"); //JPQL "type()" function 50 | 51 | createQuery("from Person p where all(select a.city from p.pastAddresses a) = 'barcelona'"); //"all" operator with correlated subquery 52 | createQuery("from Person p where any(select a.city from p.pastAddresses a) = 'barcelona'"); //"any" operator with correlated subquery 53 | createQuery("from Person p where '' = all elements(p.notes)"); //"all" operator with correlated HQL "elements()" function 54 | createQuery("from Person p where 1 < any indices(p.notes)"); //"any" operator with correlated HQL "indices()" function 55 | createQuery("from Employee e where 'boss' = some indices(e.contacts)"); //"some" operator with correlated HQL "indices()" function 56 | 57 | createQuery("select p.name, n from Person p join p.notes n where length(n)>0"); //join an element collection 58 | createQuery("from Person p where p.notes[0] is not null"); //HQL list indexing operator 59 | createQuery("from Employee e where e.contacts['boss'] is not null"); //HQL map indexing operator 60 | createQuery("from Employee e where e.contacts['boss'].id = 222"); //HQL map indexing operator 61 | 62 | createQuery("from Address add where add.country.code='au'"); 63 | 64 | createQuery("select p.whatever from Person p where p.whatever is not null"); 65 | createQuery("select p.address.country.thing from Person p where p.address.country.thing is null"); 66 | 67 | createQuery("select p2 from Person p join p.address.country.residents p2"); //join association in embeddable 68 | createQuery("from Person p where p.notes is empty"); //JPQL "is empty" operator for element collection 69 | createQuery("from Person p where p.pastAddresses is empty"); //JPQL "is empty" operator for association 70 | createQuery("from Address a where a.country.residents is empty"); //JPQL "is empty" operator for association in embeddable 71 | createQuery("from Person p where p.address.country.residents is empty"); //JPQL "is empty" operator 72 | createQuery("from Person p where p.address.currentResidents is empty"); //JPQL "is empty" operator 73 | 74 | createQuery("select a, c from Address a join a.country c"); //fake join to a component 75 | createQuery("select p, c from Person p join p.address.country c"); //fake join to a component 76 | createQuery("select c.code, c.name from Address a join a.country c"); //fake join to a component 77 | 78 | createQuery("select e from Person p join p.emails e"); 79 | createQuery("select e.address from Person p join p.emails e"); 80 | createQuery("select e from Person p join p.emails e where e.address is not null and length(e.address)>0"); 81 | 82 | createQuery("from Person p where treat(p.emergencyContact as Employee).employeeId = 2"); //JPQL "treat as" operator 83 | createQuery("from Person p join treat(p.emergencyContact as Employee) c where c.employeeId = 2"); //JPQL "treat as" operator 84 | createQuery("from Employee e join treat(e.contacts as Employee) c where c.employeeId = 2"); //JPQL "treat as" operator 85 | 86 | createQuery("from Employee e where e.emergencyContact member of e.contacts"); //JPQL "member of" operator with association 87 | createQuery("from Employee e where e.emergencyContact not member of e.contacts"); //JPQL "not member of" operator with association 88 | createQuery("from Person p where '' member of p.notes"); //JPQL "member of" operator with element collection 89 | createQuery("from Person p where '' not member of p.notes"); //JPQL "not member of" operator with element collection 90 | 91 | createQuery("select upper(p.name), year(current_date) from Person p"); 92 | createQuery("select upper(p.name), year(current_date) from Person p where year(current_date) = 2019 and upper(p.name) = 'GAVIN'"); 93 | 94 | createQuery("select nullif(e.name, ''), coalesce(e.employeeId, e.id, e.name) from Employee e"); //JPQL "nullif()" and "coalesce()" 95 | createQuery("select str(p.dob) from Person p"); //HQL "str()" function 96 | createQuery("select cast(p.dob as string) from Person p"); //SQL "cast()" function 97 | createQuery("select extract(month from p.dob), extract(year from p.dob) from Person p"); //SQL "extract()" function 98 | createQuery("select function('bit_length', e.name) from Employee e"); //JPQL "function()" passthrough 99 | 100 | // createQuery("from Person p where p.sex = test.Sex.FEMALE"); //TODO: FIX! 101 | // createQuery("from Person p where test.test.Rating.Good = test.test.Rating.Bad"); //TODO: FIX! 102 | 103 | createQuery("from Person p where p.name = ?1 and p.id > ?2"); //JPQL positional args 104 | createQuery("from Person p where p.name = :name and p.id >= :minId"); //JPQL named args 105 | 106 | // createQuery("from Person p where p.dob = {d '2008-12-31'}"); 107 | // createQuery("from Person p where p.dob = date '2008-12-31'"); 108 | createQuery("select (local datetime - datetime 1974-03-25 2:30:00) by second"); 109 | } 110 | 111 | public void okQueries() { 112 | // createQuery("select current_thing from Person"); //warning 113 | createQuery("select func(p.name), year(current_date) from Person p"); //warning 114 | createQuery("from Person p where p.name = function('custom', p.id)"); //warning 115 | } 116 | 117 | private static void createQuery(String s) {} 118 | } -------------------------------------------------------------------------------- /src/test/source/test/Pair.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | public class Pair { 4 | public Pair(Person person, Address address) {} 5 | public Pair(String string, int integer) {} 6 | } 7 | -------------------------------------------------------------------------------- /src/test/source/test/PanacheBadPerson.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.Basic; 5 | import jakarta.persistence.NamedQuery; 6 | import jakarta.persistence.NamedQueries; 7 | import io.quarkus.hibernate.orm.panache.PanacheEntity; 8 | import io.quarkus.panache.common.Sort; 9 | import io.quarkus.panache.common.Parameters; 10 | 11 | @Entity 12 | @NamedQueries({ 13 | @NamedQuery(name="ok", query="from PanacheBadPerson p where p.id=1"), 14 | @NamedQuery(name="broke", query="from PanacheBadPerson p where p.x=1") 15 | }) 16 | public class PanacheBadPerson extends PanacheEntity { 17 | public String name; 18 | @Basic(optional=false) 19 | public Sex sex; 20 | 21 | public static void badQueries() { 22 | find("missing", "stef"); // property does not exist 23 | find("name = ?1 and id = ?2", "stef"); // missing positional arg 24 | find("name = :name and id = :id", Parameters.with("name", "stef")); // missing named arg 25 | find("name"); // missing required param for name 26 | find("name = :name and id = :id and id = :bar", Parameters.with("name", "stef").and("id", "foo")); // missing named arg 27 | find("name", Sort.descending("name")); // missing positional arg 28 | list("name"); // missing positional arg 29 | stream("name"); // missing positional arg 30 | delete("name"); // missing positional arg 31 | update("name"); // missing positional arg 32 | count("name"); // missing positional arg 33 | exists("name"); // missing positional arg 34 | find("name", 123, 345); // too many params for name 35 | find("", Sort.by("missing")); // property does not exist 36 | find("", Sort.by("name").and("missing")); // property does not exist 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/source/test/PanacheBadPersonRepository.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import io.quarkus.hibernate.orm.panache.PanacheRepository; 4 | import io.quarkus.panache.common.Sort; 5 | import io.quarkus.panache.common.Parameters; 6 | 7 | public class PanacheBadPersonRepository implements PanacheRepository { 8 | 9 | public void badQueries() { 10 | find("missing", "stef"); // property does not exist 11 | find("name = ?1 and id = ?2", "stef"); // missing positional arg 12 | find("name = :name and id = :id", Parameters.with("name", "stef")); // missing named arg 13 | find("name"); // missing required param for name 14 | find("name = :name and id = :id and id = :bar", Parameters.with("name", "stef").and("id", "foo")); // missing named arg 15 | find("name", Sort.descending("name")); // missing positional arg 16 | list("name"); // missing positional arg 17 | stream("name"); // missing positional arg 18 | delete("name"); // missing positional arg 19 | update("name"); // missing positional arg 20 | count("name"); // missing positional arg 21 | exists("name"); // missing positional arg 22 | find("name", 123, 345); // too many params for name 23 | find("", Sort.by("missing")); // property does not exist 24 | find("", Sort.by("name").and("missing")); // property does not exist 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/source/test/PanachePerson.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.Basic; 5 | import jakarta.persistence.NamedQuery; 6 | import jakarta.persistence.NamedQueries; 7 | import io.quarkus.hibernate.orm.panache.PanacheEntity; 8 | import io.quarkus.panache.common.Sort; 9 | import io.quarkus.panache.common.Parameters; 10 | 11 | @Entity 12 | @NamedQueries({ 13 | @NamedQuery(name="ok", query="from PanachePerson p where p.id=1"), 14 | }) 15 | public class PanachePerson extends PanacheEntity { 16 | public String name; 17 | public String title; 18 | @Basic(optional=false) 19 | public Sex sex; 20 | public boolean completed; 21 | 22 | public static PanachePerson findByName(String name) { 23 | return find("name", name).firstResult(); 24 | } 25 | 26 | public static void goodQueries() { 27 | find("name", "foo"); 28 | find("name = ?1 and id = ?2", "foo", 2); 29 | find("name = :name and id = :id", Parameters.with("name", "foo").and("id", 2)); 30 | find("name", Sort.by("name"), "foo"); 31 | find("name = ?1 and id = ?2", Sort.by("name"), "foo", 2); 32 | find("name = :name and id = :id", Sort.by("name"), Parameters.with("name", "foo").and("id", 2)); 33 | update("name", "foo"); 34 | delete("name", "foo"); 35 | count("name", "foo"); 36 | 37 | find("order by name"); 38 | find("from Person"); 39 | String stef = ""; 40 | find("completed = false and title like ?1", "%"+stef+"%"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/source/test/PanachePersonRepository.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import io.quarkus.hibernate.orm.panache.PanacheRepository; 4 | import io.quarkus.panache.common.Sort; 5 | import io.quarkus.panache.common.Parameters; 6 | 7 | public class PanachePersonRepository implements PanacheRepository { 8 | 9 | public PanachePerson findByName(String name) { 10 | return find("name", name).firstResult(); 11 | } 12 | 13 | public void goodQueries() { 14 | find("name", "foo"); 15 | find("name = ?1 and id = ?2", "foo", 2); 16 | find("name = :name and id = :id", Parameters.with("name", "foo").and("id", 2)); 17 | find("name", Sort.by("name"), "foo"); 18 | find("name = ?1 and id = ?2", Sort.by("name"), "foo", 2); 19 | find("name = :name and id = :id", Sort.by("name"), Parameters.with("name", "foo").and("id", 2)); 20 | update("name", "foo"); 21 | delete("name", "foo"); 22 | count("name", "foo"); 23 | 24 | find("order by name"); 25 | find("from Person"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/source/test/Person.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import jakarta.persistence.Id; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Basic; 6 | import jakarta.persistence.Access; 7 | import jakarta.persistence.ElementCollection; 8 | import jakarta.persistence.OneToMany; 9 | import jakarta.persistence.ManyToOne; 10 | import jakarta.persistence.OneToOne; 11 | import jakarta.persistence.NamedQuery; 12 | import jakarta.persistence.NamedQueries; 13 | import java.util.Set; 14 | import java.util.Date; 15 | import java.util.List; 16 | 17 | import static jakarta.persistence.AccessType.PROPERTY; 18 | 19 | @Entity 20 | @NamedQueries({ 21 | @NamedQuery(name="ok", query="from Person p where p.id=1"), 22 | @NamedQuery(name="broke", query="from Person p where p.x=1") 23 | }) 24 | public class Person { 25 | @Id long id; 26 | public String name; 27 | @Basic(optional=false) 28 | public Sex sex; 29 | public Date dob; 30 | @OneToOne 31 | public Address address; 32 | @OneToMany(targetEntity = Address.class) 33 | public Set
pastAddresses; 34 | @ElementCollection 35 | public List notes; 36 | @Access(PROPERTY) 37 | public String getWhatever() { return "thing"; }; 38 | @ElementCollection 39 | public Set emails; 40 | @ManyToOne 41 | public Person emergencyContact; 42 | } 43 | -------------------------------------------------------------------------------- /src/test/source/test/Sex.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | public enum Sex { 4 | MALE, FEMALE 5 | } -------------------------------------------------------------------------------- /src/test/source/test/package-info.java: -------------------------------------------------------------------------------- 1 | @CheckHQL 2 | package test; 3 | 4 | import org.hibernate.annotations.processing.CheckHQL; 5 | -------------------------------------------------------------------------------- /src/test/source/test/test/Rating.java: -------------------------------------------------------------------------------- 1 | package test.test; 2 | 3 | public enum Rating { 4 | Good, 5 | Bad, 6 | Indifferent 7 | } -------------------------------------------------------------------------------- /src/test/source/test/test/package-info.java: -------------------------------------------------------------------------------- 1 | package test.test; 2 | --------------------------------------------------------------------------------