Skip to content

add spring data jpa @Query support #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Then the validator will check any static string argument of

- the `createQuery()` method or
- the `@NamedQuery()` annotation
- the `@Query()` annotation (`org.springframework.data.jpa.repository.Query`)

which occurs in the annotated package or class.

Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defaultTasks 'assemble', 'publishToMavenLocal', 'shadowJar', 'test'
repositories {
mavenLocal()
maven {
url = 'http://repo.maven.apache.org/maven2'
url = 'https://repo.maven.apache.org/maven2'
}
}

Expand All @@ -26,7 +26,7 @@ dependencies {
compile 'net.bytebuddy:byte-buddy:1.9.10'
compile 'org.jboss.logging:jboss-logging:3.3.2.Final'

compile 'org.codehaus.groovy:groovy:2.5.6:indy'
compile 'org.codehaus.groovy:groovy:2.5.6'

compile 'org.eclipse.jdt.core.compiler:ecj:4.6.1'
compile files(org.gradle.internal.jvm.Jvm.current().toolsJar)
Expand Down
Binary file added lib/spring-data-commons-2.1.10.RELEASE.jar
Binary file not shown.
Binary file added lib/spring-data-jpa-2.1.10.RELEASE.jar
Binary file not shown.
55 changes: 52 additions & 3 deletions src/main/java/org/hibernate/query/validator/ECJProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
import static org.eclipse.jdt.internal.compiler.util.Util.searchColumnNumber;
import static org.hibernate.query.validator.ECJSessionFactory.getAnnotation;
import static org.hibernate.query.validator.ECJSessionFactory.qualifiedName;
import static org.hibernate.query.validator.HQLProcessor.CHECK_HQL;
import static org.hibernate.query.validator.HQLProcessor.jpa;
import static org.hibernate.query.validator.HQLProcessor.*;
import static org.hibernate.query.validator.Validation.validate;

/**
Expand All @@ -42,6 +41,30 @@
//@SupportedAnnotationTypes(CHECK_HQL)
public class ECJProcessor extends AbstractProcessor {

/**
* Checks if the given annotation has a {@code nativeQuery} attribute that is set as true.
*
* @param annotation NormalAnnotation
* @return true only if {@code nativeQuery} exists and {@code nativeQuery}'s value is {@code true}
*/
static boolean isNativeQuery(NormalAnnotation annotation) {
for (MemberValuePair memberValuePair : annotation.memberValuePairs()) {
if (new String(memberValuePair.name).intern().equals("nativeQuery")) {
return memberValuePair.value.constant.stringValue().equals("true");
}
}
return false;
}

static Expression getMemberValueOfAnnotation(NormalAnnotation annotation) {
for (MemberValuePair memberValuePair : annotation.memberValuePairs()) {
if (new String(memberValuePair.name).intern().equals("value")) {
return memberValuePair.value;
}
}
return null;
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Compiler compiler = ((BaseProcessingEnvImpl) processingEnv).getCompiler();
Expand Down Expand Up @@ -122,14 +145,40 @@ public boolean visit(MemberValuePair pair, BlockScope scope) {
return true;
}

@Override
public boolean visit(SingleMemberAnnotation annotation, BlockScope scope) {
if (qualifiedName(annotation.resolvedType)
.equals(SPRING_QUERY_ANNOTATION)) {
// As this is a SingleMemberAnnotation it's guaranteed that this is not a nativeQuery
if (annotation.memberValue instanceof StringLiteral) {
check((StringLiteral) annotation.memberValue, false);
}
}
return super.visit(annotation, scope);
}

@Override
public boolean visit(NormalAnnotation annotation, BlockScope scope) {
if (qualifiedName(annotation.resolvedType)
.equals(SPRING_QUERY_ANNOTATION)) {
// We need to make sure that the query is not a native query
if (!isNativeQuery(annotation)) {
final Expression memberValue = getMemberValueOfAnnotation(annotation);
if (memberValue instanceof StringLiteral) {
check((StringLiteral) memberValue, false);
}
}
}
return super.visit(annotation, scope);
}

void check(StringLiteral stringLiteral, boolean inCreateQueryMethod) {
String hql = charToString(stringLiteral.source());
ErrorReporter handler = new ErrorReporter(stringLiteral, unit, compiler);
validate(hql, inCreateQueryMethod && immediatelyCalled,
setParameterLabels, setParameterNames, handler,
new ECJSessionFactory(whitelist, handler, unit));
}

}, unit.scope);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.hibernate.query.validator

import antlr.RecognitionException
import org.eclipse.jdt.internal.compiler.ast.Expression
import org.eclipse.jdt.internal.compiler.ast.NormalAnnotation
import org.eclipse.jdt.internal.compiler.ast.SingleMemberAnnotation
import org.eclipse.jdt.internal.compiler.ast.StringLiteral
import org.hibernate.QueryException

import javax.annotation.processing.AbstractProcessor
Expand All @@ -12,6 +16,7 @@ import static java.lang.Integer.parseInt
import static java.util.Collections.emptyList
import static org.hibernate.query.validator.EclipseSessionFactory.*
import static org.hibernate.query.validator.HQLProcessor.CHECK_HQL
import static org.hibernate.query.validator.HQLProcessor.SPRING_QUERY_ANNOTATION
import static org.hibernate.query.validator.HQLProcessor.jpa
import static org.hibernate.query.validator.Validation.validate

Expand Down Expand Up @@ -120,7 +125,9 @@ class EclipseProcessor extends AbstractProcessor {
for (type in unit.types) {
if (isCheckable(type.binding, unit)) {
whitelist = getWhitelist(type.binding, unit, compiler)
type.annotations.each { annotation ->
getAnnotationsInType(type)
.flatten()
.each { annotation ->
switch (qualifiedTypeName(annotation.resolvedType)) {
case jpa("NamedQuery"):
annotation.memberValuePairs.each { pair ->
Expand All @@ -138,6 +145,21 @@ class EclipseProcessor extends AbstractProcessor {
}
}
break
case SPRING_QUERY_ANNOTATION:
if (annotation instanceof SingleMemberAnnotation) {
if (annotation.memberValue instanceof StringLiteral) {
validateArgument(annotation.memberValue, false)
}
}
else if (annotation instanceof NormalAnnotation) {
if (!ECJProcessor.isNativeQuery(annotation)) {
Expression memberValue = ECJProcessor.getMemberValueOfAnnotation(annotation)
if (memberValue instanceof StringLiteral) {
validateArgument(memberValue, false)
}
}
}
break
}
}
type.methods.each { method ->
Expand All @@ -147,6 +169,21 @@ class EclipseProcessor extends AbstractProcessor {
}
}

/**
* @return a list of annotations both on the type and on the method. Returns empty list if there is no annotation.
*/
private static Collection<?> getAnnotationsInType(def type) {
def result = []
if (type.annotations != null) {
result += type.annotations
}
// We need method level annotations to support Spring's @Query
if (type.methods != null && type.methods.annotations) {
result += type.methods.annotations
}
return result - null
}

private void validateStatements(statements) {
statements.each { statement -> validateStatement(statement) }
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/hibernate/query/validator/HQLProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class HQLProcessor extends AbstractProcessor {

static final String CHECK_HQL = "org.hibernate.query.validator.CheckHQL";

static final String SPRING_QUERY_ANNOTATION = "org.springframework.data.jpa.repository.Query";

static String jpa(String name) {
//sneak it past shadow
return new StringBuilder("javax.")
Expand Down
53 changes: 36 additions & 17 deletions src/main/java/org/hibernate/query/validator/JavacProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
import java.util.List;
import java.util.Set;

import static org.hibernate.query.validator.HQLProcessor.CHECK_HQL;
import static org.hibernate.query.validator.HQLProcessor.jpa;
import static org.hibernate.query.validator.HQLProcessor.*;
import static org.hibernate.query.validator.Validation.validate;

/**
Expand Down Expand Up @@ -77,6 +76,22 @@ private void check(JCTree.JCLiteral jcLiteral, String hql,
(JavacProcessingEnvironment) processingEnv));
}

private void checkAnnotation(String argName, List<JCTree.JCExpression> jcAnnotations) {
for (JCTree.JCExpression arg : jcAnnotations) {
if (arg instanceof JCTree.JCAssign) {
JCTree.JCAssign assign = (JCTree.JCAssign) arg;
if (argName.equals(assign.lhs.toString())
&& assign.rhs instanceof JCTree.JCLiteral) {
JCTree.JCLiteral jcLiteral =
(JCTree.JCLiteral) assign.rhs;
if (jcLiteral.value instanceof String) {
check(jcLiteral, (String) jcLiteral.value, false);
}
}
}
}
}

JCTree.JCLiteral firstArgument(JCTree.JCMethodInvocation call) {
for (JCTree.JCExpression e : call.args) {
return e instanceof JCTree.JCLiteral ?
Expand Down Expand Up @@ -124,21 +139,25 @@ public void visitApply(JCTree.JCMethodInvocation jcMethodInvocation) {
public void visitAnnotation(JCTree.JCAnnotation jcAnnotation) {
AnnotationMirror annotation = jcAnnotation.attribute;
String name = annotation.getAnnotationType().toString();
if (name.equals(jpa("NamedQuery"))) {
for (JCTree.JCExpression arg : jcAnnotation.args) {
if (arg instanceof JCTree.JCAssign) {
JCTree.JCAssign assign = (JCTree.JCAssign) arg;
if ("query".equals(assign.lhs.toString())
&& assign.rhs instanceof JCTree.JCLiteral) {
JCTree.JCLiteral jcLiteral =
(JCTree.JCLiteral) assign.rhs;
if (jcLiteral.value instanceof String) {
check(jcLiteral, (String) jcLiteral.value, false);
}
}
}
}
} else {
if (name.equals(jpa("NamedQuery"))) {
checkAnnotation("query", jcAnnotation.args);
}
else if (name.equals(SPRING_QUERY_ANNOTATION)) {
// We need to make sure that the query is not a native query
for (JCTree.JCExpression arg : jcAnnotation.args) {
if (arg instanceof JCTree.JCAssign) {
JCTree.JCAssign assign = (JCTree.JCAssign) arg;
if ("nativeQuery".equals(assign.lhs.toString())) {
JCTree.JCLiteral jcLiteral = (JCTree.JCLiteral) assign.rhs;
Integer isNativeQuery = (Integer) jcLiteral.value;
if (isNativeQuery == 1) {
return; // need to skip check if it's a native query
}
}
}
}
checkAnnotation("value", jcAnnotation.args);
} else {
super.visitAnnotation(jcAnnotation); //needed!
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class HQLValidationTest {

@Test
public void testJavac() throws Exception {
String errors = compileWithJavac("test", "test.test");
String errors = compileWithJavac("test", "test.test", "test.spring");

assertFalse(errors.contains("GoodQueries.java:"));

Expand Down Expand Up @@ -73,11 +73,16 @@ public void testJavac() throws Exception {

assertTrue(errors.contains("BadQueries.java:48: warning: :hello does not occur in the query"));

// Spring Data @Query annotation checks
assertTrue(errors.contains("AddressRepository.java:13: error: unexpected token: *"));
assertTrue(errors.contains("AddressRepository.java:16: error: Address has no mapped citi"));
assertTrue(errors.contains("AddressRepository.java:19: error: Address has no mapped zix"));
assertFalse(errors.contains("AddressRepositoryGoodQueries.java:"));
}

@Test
public void testECJ() throws Exception {
String errors = compileWithECJ("test", "test.test");
String errors = compileWithECJ("test", "test.test", "test.spring");

assertFalse(errors.contains("GoodQueries.java"));

Expand Down Expand Up @@ -125,12 +130,17 @@ public void testECJ() throws Exception {

assertTrue(errors.contains(":hello does not occur in the query") && errors.contains("BadQueries.java (at line 48)"));

// Spring Data @Query annotation checks
assertTrue(errors.contains("AddressRepository.java (at line 13)") && errors.contains("unexpected token: *"));
assertTrue(errors.contains("AddressRepository.java (at line 16)") && errors.contains("Address has no mapped citi"));
assertTrue(errors.contains("AddressRepository.java (at line 19)") && errors.contains("Address has no mapped zix"));
assertFalse(errors.contains("AddressRepositoryGoodQueries"));
}

@Test
public void testEclipse() throws Exception {
forceEclipseForTesting = true;
String errors = compileWithECJ("test", "test.test");
String errors = compileWithECJ("test", "test.test", "test.spring");

assertFalse(errors.contains("GoodQueries.java"));

Expand Down Expand Up @@ -178,6 +188,12 @@ public void testEclipse() throws Exception {

assertTrue(errors.contains(":hello does not occur in the query") && errors.contains("BadQueries.java (at line 48)"));

// Spring Data @Query annotation checks
assertTrue(errors.contains("AddressRepository.java (at line 13)") && errors.contains("unexpected token: *"));
assertTrue(errors.contains("AddressRepository.java (at line 16)") && errors.contains("Address has no mapped citi"));
assertTrue(errors.contains("AddressRepository.java (at line 19)") && errors.contains("Address has no mapped zix"));
assertFalse(errors.contains("AddressRepositoryGoodQueries"));

forceEclipseForTesting = false;
}

Expand Down
21 changes: 21 additions & 0 deletions src/test/source/test/spring/AddressRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package test.spring;

import org.springframework.data.jpa.repository.Query;
import test.Address;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface AddressRepository extends CrudRepository<Address, Long> {

Optional<Address> findByZip(String zip);

@Query("SELECT * FROM Address")
Optional<Address> badQuery();

@Query("SELECT a FROM Address a WHERE a.citi = :city")
Optional<Address> goodQuery();

@Query(value = "SELECT a FROM Address a WHERE a.zix = :zip", countName = "countNq")
Optional<Address> goodQueryWithMultipleAnnotationAttribute();
}
21 changes: 21 additions & 0 deletions src/test/source/test/spring/AddressRepositoryGoodQueries.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package test.spring;

import org.springframework.data.jpa.repository.Query;
import test.Address;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface AddressRepositoryGoodQueries extends CrudRepository<Address, Long> {

Optional<Address> findByZip(String zip);

@Query("SELECT a FROM Address a WHERE a.city = :city")
Optional<Address> goodQuery();

@Query("SELECT a FROM Address a WHERE a.city = :city")
Optional<Address> anotherGoodQuery(String city);

@Query(value = "SELECT a FROM Address a WHERE a.citi = :city", nativeQuery = true)
Optional<Address> goodQueryNative();
}
Loading