Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ public boolean isEnabledCodeLensOverDataQueryMethods() {
return Boolean.TRUE.equals(b);
}

public String getDataQueryStyle() {
String style = settings.getString("boot-java", "code-action", "data-query-style");
return style == null ? "compact" : style;
}

public boolean isEnabledCodeLensForWebConfigs() {
Boolean b = settings.getBoolean("boot-java", "java", "codelens-web-configs-on-controller-classes");
return Boolean.TRUE.equals(b);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public class RewriteConfig {
}

@ConditionalOnBean(RewriteRefactorings.class)
@Bean QueryMethodCodeActionProvider queryMethodCodeActionProvider(DataRepositoryAotMetadataService dataRepoAotService, RewriteRefactorings refactorings) {
return new QueryMethodCodeActionProvider(dataRepoAotService, refactorings);
@Bean QueryMethodCodeActionProvider queryMethodCodeActionProvider(DataRepositoryAotMetadataService dataRepoAotService, RewriteRefactorings refactorings, BootJavaConfig config) {
return new QueryMethodCodeActionProvider(dataRepoAotService, refactorings, config);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
Expand All @@ -32,6 +33,9 @@
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.data.formatter.JpqlQueryFormatter;
import org.springframework.ide.vscode.boot.java.data.formatter.MongoQueryFormatter;
import org.springframework.ide.vscode.boot.java.data.formatter.QueryFormatter;
import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
Expand All @@ -56,6 +60,9 @@ public class DataRepositoryAotMetadataCodeLensProvider implements CodeLensProvid
DataRepositoryModule.JDBC, Annotations.DATA_JDBC_QUERY,
DataRepositoryModule.MONGODB, Annotations.DATA_MONGODB_QUERY
);

private static final QueryFormatter JPQL_FORMATTER = new JpqlQueryFormatter();
private static final QueryFormatter MONGO_FORMATTER = new MongoQueryFormatter();

private static final Logger log = LoggerFactory.getLogger(DataRepositoryAotMetadataCodeLensProvider.class);

Expand Down Expand Up @@ -155,7 +162,7 @@ private List<CodeLens> createCodeLenses(IJavaProject project, MethodDeclaration
|| hierarchyAnnot.isAnnotatedWith(mb, Annotations.DATA_JDBC_QUERY);

if (!isQueryAnnotated) {
codeLenses.add(new CodeLens(range, refactorings.createFixCommand(COVERT_TO_QUERY_LABEL, createFixDescriptor(mb, document.getUri(), metadata.module(), methodMetadata)), null));
codeLenses.add(new CodeLens(range, refactorings.createFixCommand(COVERT_TO_QUERY_LABEL, createFixDescriptor(mb, document.getUri(), metadata.module(), methodMetadata, config)), null));
}

Command impl = new Command("Go To Implementation", GenAotQueryMethodImplProvider.CMD_NAVIGATE_TO_IMPL, List.of(new GenAotQueryMethodImplProvider.GoToImplParams(
Expand Down Expand Up @@ -196,7 +203,7 @@ private Optional<CodeLens> createRefreshCodeLens(IJavaProject project, String ti
});
}

static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataRepositoryModule module, IDataRepositoryAotMethodMetadata methodMetadata) {
static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataRepositoryModule module, IDataRepositoryAotMethodMetadata methodMetadata, BootJavaConfig config) {
return new FixDescriptor(AddAnnotationOverMethod.class.getName(), List.of(docUri), "Turn into `@Query`")
.withRecipeScope(RecipeScope.FILE)
.withParameters(Map.of(
Expand All @@ -205,19 +212,33 @@ static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataR
Arrays.stream(mb.getParameterTypes())
.map(pt -> pt.getQualifiedName())
.collect(Collectors.joining(", "))),
"attributes", createAttributeList(methodMetadata.getAttributesMap())));
"attributes", createAttributeList(methodMetadata.getAttributesMap(), module, config)));
}

private static List<AddAnnotationOverMethod.Attribute> createAttributeList(Map<String, String> attributes) {
private static List<AddAnnotationOverMethod.Attribute> createAttributeList(Map<String, String> attributes, DataRepositoryModule module, BootJavaConfig config) {
List<AddAnnotationOverMethod.Attribute> result = new ArrayList<>();
String style = config.getDataQueryStyle();
boolean isMultiline = "multiline".equalsIgnoreCase(style);

if (style != null && !"compact".equalsIgnoreCase(style) && !isMultiline) {
log.warn("Unknown data-query-style: '{}'. Falling back to 'compact'.", style);
}

for (Map.Entry<String, String> entry : attributes.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (value == null) continue;

result.add(new AddAnnotationOverMethod.Attribute(key, "\"\"\"\n" + value + "\n\"\"\""));
if (isMultiline) {
QueryFormatter formatter = (module == DataRepositoryModule.MONGODB) ? MONGO_FORMATTER : JPQL_FORMATTER;
value = formatter.format(value);
value = "\"\"\"" + value + "\n \"\"\"";
} else {
value = "\"" + StringEscapeUtils.escapeJava(value).trim() + "\"";
}

result.add(new AddAnnotationOverMethod.Attribute(key, value));
}
return result;
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionKind;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.codeaction.JdtAstCodeActionProvider;
Expand All @@ -36,10 +37,12 @@ public class QueryMethodCodeActionProvider implements JdtAstCodeActionProvider {

private final DataRepositoryAotMetadataService repositoryMetadataService;
private final RewriteRefactorings refactorings;
private final BootJavaConfig config;

public QueryMethodCodeActionProvider(DataRepositoryAotMetadataService repositoryMetadataService, RewriteRefactorings refactorings) {
public QueryMethodCodeActionProvider(DataRepositoryAotMetadataService repositoryMetadataService, RewriteRefactorings refactorings, BootJavaConfig config) {
this.repositoryMetadataService = repositoryMetadataService;
this.refactorings = refactorings;
this.config = config;
}

@Override
Expand Down Expand Up @@ -94,7 +97,7 @@ public boolean visit(MethodDeclaration node) {

private CodeAction createCodeAction(IMethodBinding mb, URI docUri, DataRepositoryAotMetadata metadata, IDataRepositoryAotMethodMetadata method) {
CodeAction ca = new CodeAction();
ca.setCommand(refactorings.createFixCommand(TITLE, DataRepositoryAotMetadataCodeLensProvider.createFixDescriptor(mb, docUri.toASCIIString(), metadata.module(), method)));
ca.setCommand(refactorings.createFixCommand(TITLE, DataRepositoryAotMetadataCodeLensProvider.createFixDescriptor(mb, docUri.toASCIIString(), metadata.module(), method, config)));
ca.setTitle(TITLE);
ca.setKind(CodeActionKind.Refactor);
return ca;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*******************************************************************************
* Copyright (c) 2026 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.formatter;

import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.ConsoleErrorListener;
import org.antlr.v4.runtime.Token;
import org.springframework.ide.vscode.parser.jpql.JpqlLexer;
import org.springframework.ide.vscode.parser.jpql.JpqlParser;

public class JpqlQueryFormatter implements QueryFormatter {

@Override
public String format(String query) {
try {
JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(query));
lexer.removeErrorListener(ConsoleErrorListener.INSTANCE);

StringBuilder sb = new StringBuilder();

int lastStopIndex = -1;
for (Token token = lexer.nextToken(); token.getType() != Token.EOF; token = lexer.nextToken()) {
boolean newlineAdded = false;
if (isNewlineKeyword(token)) {
sb.append("\n ");
newlineAdded = true;
}

if (!newlineAdded && lastStopIndex != -1 && token.getStartIndex() > lastStopIndex + 1) {
sb.append(" ");
} else if (!newlineAdded && sb.length() == 0) {
sb.append("\n ");
}

sb.append(token.getText());
lastStopIndex = token.getStopIndex();
}
return sb.toString();
} catch (Exception e) {
return "\n " + query;
}
}

private boolean isNewlineKeyword(Token token) {
int type = token.getType();
if (type == JpqlParser.SELECT || type == JpqlParser.FROM || type == JpqlParser.WHERE
|| type == JpqlParser.ORDER || type == JpqlParser.GROUP || type == JpqlParser.HAVING
|| type == JpqlParser.UPDATE || type == JpqlParser.DELETE
|| type == JpqlParser.SET
|| type == JpqlParser.JOIN || type == JpqlParser.LEFT || type == JpqlParser.INNER) {
return true;
}
String text = token.getText();
if ("INSERT".equalsIgnoreCase(text) || "VALUES".equalsIgnoreCase(text) || "RIGHT".equalsIgnoreCase(text)) {
return true;
}
return false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*******************************************************************************
* Copyright (c) 2026 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.formatter;

import org.json.JSONObject;

public class MongoQueryFormatter implements QueryFormatter {

@Override
public String format(String query) {
try {
// Try to format as JSON
String formattedJson = new JSONObject(query).toString(4);
// Indent the formatted JSON to align with Java code (8 spaces)
return "\n " + formattedJson.replace("\n", "\n ");
} catch (Exception e) {
// Fallback: just indent
return "\n " + query;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*******************************************************************************
* Copyright (c) 2026 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.formatter;

/**
* Interface for formatting data repository queries.
*/
public interface QueryFormatter {

String format(String query);

}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ void noCodeLensOverMethodWithQueryAnnotation() throws Exception {

@Test
void turnIntoQueryUsesTextBlock() throws Exception {
harness.changeConfiguration(new Settings(new Gson()
.toJsonTree(Map.of("boot-java", Map.of(
"java", Map.of("codelens-over-query-methods", true),
"code-action", Map.of("data-query-style", "multiline")
)))));

Path filePath = Paths.get(testProject.getLocationUri())
.resolve("src/main/java/example/springdata/aot/CategoryRepository.java");

Expand All @@ -115,6 +121,34 @@ void turnIntoQueryUsesTextBlock() throws Exception {
assertNotNull(queryValue, "Query value should not be null");

assertTrue(queryValue.startsWith("\"\"\""), "Query should be generated as a text block");
assertTrue(queryValue.contains("\n"), "Query should be split into multiple lines");
assertTrue(queryValue.contains("\n SELECT"), "Should have newline and 8 spaces before SELECT");
assertTrue(queryValue.contains("\n FROM"), "Should have newline and 8 spaces before FROM");
assertTrue(queryValue.contains("\n WHERE"), "Should have newline and 8 spaces before WHERE");
assertTrue(queryValue.endsWith("\n \"\"\""), "Should end with newline and 4 spaces before closing triple quotes");
}

@Test
void turnIntoQueryUsesCompactStyleByDefault() throws Exception {
Path filePath = Paths.get(testProject.getLocationUri())
.resolve("src/main/java/example/springdata/aot/CategoryRepository.java");

Editor editor = harness.newEditor(
LanguageId.JAVA,
new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8),
filePath.toUri().toASCIIString()
);

List<CodeLens> cls = editor.getCodeLenses("findAllByNameContaining", 1);

String queryValue = extractValueFromAttributes(cls.get(0));

System.out.println("Extracted query value: " + queryValue);
assertNotNull(queryValue, "Query value should not be null");

assertTrue(queryValue.startsWith("\""), "Query should be generated as a string literal");
assertFalse(queryValue.startsWith("\"\"\""), "Query should NOT be generated as a text block");
assertFalse(queryValue.contains("\n SELECT"), "Should NOT have formatted newline before SELECT");
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ void convertToQueryCodeAction() throws Exception {
String rawText = docEdit.getEdits().get(0).getNewText();

assertEquals(
"@Query(\"\"\"\nSELECT u FROM users u WHERE u.lastname LIKE :lastname ESCAPE '\\' ORDER BY u.firstname asc\n\"\"\")",
"@Query(\"SELECT u FROM users u WHERE u.lastname LIKE :lastname ESCAPE '\\\\' ORDER BY u.firstname asc\")",
rawText.trim());
assertEquals(filePath.toUri().toASCIIString(), docEdit.getTextDocument().getUri());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,8 @@ void convertToQueryCodeAction() throws Exception {
String rawText = docEdit.getEdits().get(0).getNewText().trim();

assertEquals(
"@Query(\"\"\"\n" +
"{\"lastname\": /^\\Q?0\\E/}\n" +
"\"\"\")\n"
+ " Page<UserProjection> findUserByLastnameStartingWith(String lastname, Pageable page)",
rawText.replace("\r\n", "\n"));
"@Query(\"{\\\"lastname\\\": /^\\\\Q?0\\\\E/}\")",
rawText);
assertEquals(filePath.toUri().toASCIIString(), docEdit.getTextDocument().getUri());
}

Expand Down