Skip to content

Commit

Permalink
Syntax highlighting and validation inside @NamedQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
BoykoAlex committed Jul 25, 2024
1 parent 2b91b4c commit c60d41d
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class Annotations {
public static final String JPA_JAVAX_EMBEDDED_ID = "javax.persistence.EmbeddedId";
public static final String JPA_JAKARTA_ID_CLASS = "jakarta.persistence.IdClass";
public static final String JPA_JAVAX_ID_CLASS = "javax.persistence.IdClass";
public static final String JPA_JAKARTA_NAMED_QUERY = "jakarta.persistence.NamedQuery";
public static final String JPA_JAVAX_NAMED_QUERY = "javax.persistence.NamedQuery";
public static final String DATA_QUERY = "org.springframework.data.jpa.repository.Query";

public static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TextBlock;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.JdtSemanticTokensProvider;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
Expand All @@ -41,7 +44,7 @@
public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProvider {

private static final String QUERY = "Query";
private static final String FQN_QUERY = "org.springframework.data.jpa.repository." + QUERY;
private static final String NAMED_QUERY = "NamedQuery";

private final JpqlSemanticTokens jpqlProvider;
private final HqlSemanticTokens hqlProvider;
Expand Down Expand Up @@ -75,12 +78,10 @@ public ASTVisitor getTokensComputer(IJavaProject jp, TextDocument doc, Compilati
return new ASTVisitor() {
@Override
public boolean visit(NormalAnnotation a) {
Expression queryExpression = null;
boolean isNative = false;
if (isQueryAnnotation(a)) {
List<?> values = a.values();

Expression queryExpression = null;
boolean isNative = false;
for (Object value : values) {
for (Object value : a.values()) {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
String name = pair.getName().getFullyQualifiedName();
Expand All @@ -102,17 +103,30 @@ public boolean visit(NormalAnnotation a) {
}
}
}

if (queryExpression != null) {
if (isNative) {
computeTokensForQueryExpression(getSqlSemanticTokensProvider(jp), queryExpression).forEach(tokensData::accept);
} else {
computeTokensForQueryExpression(provider, queryExpression).forEach(tokensData::accept);
} else if (isNamedQueryAnnotation(a)) {
for (Object value : a.values()) {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
String name = pair.getName().getFullyQualifiedName();
if (name != null) {
switch (name) {
case "query":
queryExpression = pair.getValue();
break;
}
}
}
}

return false;
}

if (queryExpression != null) {
if (isNative) {
computeTokensForQueryExpression(getSqlSemanticTokensProvider(jp), queryExpression).forEach(tokensData::accept);
} else {
computeTokensForQueryExpression(provider, queryExpression).forEach(tokensData::accept);
}
}

return false;
}

Expand Down Expand Up @@ -159,14 +173,33 @@ private static List<SemanticTokenData> computeTokensForQueryExpression(SemanticT
}


private static boolean isQueryAnnotation(Annotation a) {
return FQN_QUERY.equals(a.getTypeName().getFullyQualifiedName())
|| QUERY.equals(a.getTypeName().getFullyQualifiedName());
static boolean isQueryAnnotation(Annotation a) {
if (Annotations.DATA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) {
ITypeBinding type = a.resolveTypeBinding();
if (type != null) {
return AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.DATA_QUERY);
}
}
return false;
}

static boolean isNamedQueryAnnotation(Annotation a) {
if (NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || Annotations.JPA_JAKARTA_NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName())
|| Annotations.JPA_JAVAX_NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName())) {
ITypeBinding type = a.resolveTypeBinding();
if (type != null) {
return AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.JPA_JAKARTA_NAMED_QUERY)
|| AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.JPA_JAVAX_NAMED_QUERY);
}
}
return false;
}

@Override
public boolean isApplicable(IJavaProject project) {
return supportState.isEnabled() && SpringProjectUtil.hasDependencyStartingWith(project, "spring-data-jpa", null);
return supportState.isEnabled() && (SpringProjectUtil.hasDependencyStartingWith(project, "spring-data-jpa", null)
|| SpringProjectUtil.hasDependencyStartingWith(project, "jakarta.persistence-api", null)
|| SpringProjectUtil.hasDependencyStartingWith(project, "javax.persistence-api", null));
}

private SemanticTokensDataProvider getSqlSemanticTokensProvider(IJavaProject project) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import java.net.URI;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

Expand All @@ -26,8 +25,6 @@
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TextBlock;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.handlers.Reconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException;
Expand Down Expand Up @@ -65,32 +62,42 @@ public ASTVisitor createVisitor(IJavaProject project, URI docURI, CompilationUni
@Override
public boolean visit(NormalAnnotation node) {

if (!AnnotationHierarchies.hasTransitiveSuperAnnotationType(node.resolveTypeBinding(), Annotations.DATA_QUERY)) {
return false;
}

List<?> values = node.values();

Expression queryExpression = null;
boolean isNative = false;
for (Object value : values) {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
String name = pair.getName().getFullyQualifiedName();
if (name != null) {
switch (name) {
case "value":
queryExpression = pair.getValue();
break;
case "nativeQuery":
Expression expression = pair.getValue();
if (expression != null) {
Object o = expression.resolveConstantExpressionValue();
if (o instanceof Boolean b) {
isNative = b.booleanValue();
if (JdtDataQuerySemanticTokensProvider.isQueryAnnotation(node)) {
for (Object value : node.values()) {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
String name = pair.getName().getFullyQualifiedName();
if (name != null) {
switch (name) {
case "value":
queryExpression = pair.getValue();
break;
case "nativeQuery":
Expression expression = pair.getValue();
if (expression != null) {
Object o = expression.resolveConstantExpressionValue();
if (o instanceof Boolean b) {
isNative = b.booleanValue();
}
}
break;
}
}
}
}
} else if (JdtDataQuerySemanticTokensProvider.isNamedQueryAnnotation(node)) {
for (Object value : node.values()) {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
String name = pair.getName().getFullyQualifiedName();
if (name != null) {
switch (name) {
case "query":
queryExpression = pair.getValue();
break;
}
break;
}
}
}
Expand All @@ -110,11 +117,9 @@ public boolean visit(NormalAnnotation node) {

@Override
public boolean visit(SingleMemberAnnotation node) {
if (!AnnotationHierarchies.hasTransitiveSuperAnnotationType(node.resolveTypeBinding(), Annotations.DATA_QUERY)) {
return false;
if (JdtDataQuerySemanticTokensProvider.isQueryAnnotation(node)) {
reconcileExpression(getQueryReconciler(project), node.getValue(), problemCollector);
}

reconcileExpression(getQueryReconciler(project), node.getValue(), problemCollector);
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,4 +435,49 @@ public interface OwnerRepository {
assertThat(source.substring(token.start(), token.end())).isEqualTo("}");
assertThat(token).isEqualTo(new SemanticTokenData(176, 177, "operator", new String[0]));
}

@Test
void namedQueryAnnotation() throws Exception {
String source = """
package my.package
import jakarta.persistence.NamedQuery;
@NamedQuery(name = " my_query", query = "SELECT DISTINCT owner FROM Owner owner")
public interface OwnerRepository {
}
""";

String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString();
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp);

assertThat(cu).isNotNull();

List<SemanticTokenData> tokens = computeTokens(cu);

SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(101, 107, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("SELECT");

token = tokens.get(1);
assertThat(token).isEqualTo(new SemanticTokenData(108, 116, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("DISTINCT");

token = tokens.get(2);
assertThat(token).isEqualTo(new SemanticTokenData(117, 122, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");

token = tokens.get(3);
assertThat(token).isEqualTo(new SemanticTokenData(123, 127, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("FROM");

token = tokens.get(4);
assertThat(token).isEqualTo(new SemanticTokenData(128, 133, "class", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("Owner");

token = tokens.get(5);
assertThat(token).isEqualTo(new SemanticTokenData(134, 139, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -278,4 +278,22 @@ public interface OwnerRepository extends Repository<Object, Integer> {
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
editor.assertProblems();
}

@Test
void namedQueryAnnotation() throws Exception {
String source = """
package my.package
import jakarta.persistence.NamedQuery;
@NamedQuery(name = " my_query", query = "SELECTX ptype FROM PetType ptype ORDER BY ptype.name")
public interface OwnerRepository {
}
""";
String docUri = directory.toPath().resolve("src/main/java/example/demo/OwnerRepository.java").toUri()
.toString();
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
editor.assertProblems("SELECTX|HQL: mismatched input 'SELECTX'");
}

}

0 comments on commit c60d41d

Please # to comment.