Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ public class Constants {
public static final String PREF_CODELENS_QUERY_METHODS = "boot-java.java.codelens-over-query-methods";

public static final String PREF_CODELENS_WEB_CONFIGS_ON_CONTROLLER_CLASSES = "boot-java.java.codelens-web-configs-on-controller-classes";


public static final String PREF_DATA_QUERY_MULTILINE = "boot-java.code-action.data-query-multiline";

public static final String PREF_AI_MCP_ENABLED = "boot-java.ai.mcp-server-enabled";
public static final String PREF_AI_MCP_PORT = "boot-java.ai.mcp-server-port";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ private void sendConfiguration() {
bootJavaObj.put("java", javaSettings);
bootJavaObj.put("remote-apps", getAllRemoteApps());
bootJavaObj.put("modulith-project-tracking", preferenceStore.getBoolean(Constants.PREF_MODULITH));
bootJavaObj.put("code-action", Map.of("data-query-multiline", preferenceStore.getBoolean(Constants.PREF_DATA_QUERY_MULTILINE)));

bootJavaObj.put("rewrite", Map.of(
"recipe-filters", StringListEditor.decode(preferenceStore.getString(Constants.PREF_REWRITE_RECIPE_FILTERS)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ protected void createFieldEditors() {

// Show AOT generated query over Data Query methods
addField(new BooleanFieldEditor(Constants.PREF_CODELENS_QUERY_METHODS, "Show CodeLens with AOT generated query over Data Query methods", fieldEditorParent));

// Data Query Multiline
addField(new BooleanFieldEditor(Constants.PREF_DATA_QUERY_MULTILINE, "Generate @Query as multiline text block", fieldEditorParent));

// Show Web Config code lens
addField(new BooleanFieldEditor(Constants.PREF_CODELENS_WEB_CONFIGS_ON_CONTROLLER_CLASSES, "Show CodeLens with Web Config Details for controllers", fieldEditorParent));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public void initializeDefaultPreferences() {
preferenceStore.setDefault(Constants.PREF_SYMBOLS_FROM_NEW_INDEX, true);
preferenceStore.setDefault(Constants.PREF_CODELENS_QUERY_METHODS, true);
preferenceStore.setDefault(Constants.PREF_CODELENS_WEB_CONFIGS_ON_CONTROLLER_CLASSES, true);

preferenceStore.setDefault(Constants.PREF_DATA_QUERY_MULTILINE, false);

preferenceStore.setDefault(Constants.PREF_AI_MCP_ENABLED, false);
preferenceStore.setDefault(Constants.PREF_AI_MCP_PORT, 50627);
}
Expand Down
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 boolean isDataQueryMultiline() {
Boolean b = settings.getBoolean("boot-java", "code-action", "data-query-multiline");
return Boolean.TRUE.equals(b);
}

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 @@ -27,12 +28,15 @@
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.handlers.CodeLensProvider;
import org.springframework.ide.vscode.parser.hql.HqlQueryFormatter;
import org.springframework.ide.vscode.parser.postgresql.PostgreSqlQueryFormatter;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
import org.springframework.ide.vscode.commons.java.IJavaProject;
Expand All @@ -56,7 +60,7 @@ public class DataRepositoryAotMetadataCodeLensProvider implements CodeLensProvid
DataRepositoryModule.JDBC, Annotations.DATA_JDBC_QUERY,
DataRepositoryModule.MONGODB, Annotations.DATA_MONGODB_QUERY
);

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

private final DataRepositoryAotMetadataService repositoryMetadataService;
Expand Down Expand Up @@ -155,7 +159,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 +200,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 +209,37 @@ 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<>();
boolean isMultiline = config.isDataQueryMultiline();

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) {
String formattedValue = switch (module) {
case JPA -> new HqlQueryFormatter().format(value);
case JDBC -> new PostgreSqlQueryFormatter().format(value);
case MONGODB -> {
try {
yield new JSONObject(value).toString(4);
} catch (Exception e) {
yield value;
}
}
};
value = "\"\"\"\n" + formattedValue + "\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 @@ -18,7 +18,9 @@
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionKind;
import org.eclipse.lsp4j.Command;
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 +38,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 +98,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
Expand Up @@ -92,7 +92,7 @@ public Command createFixCommand(String title, FixDescriptor f) {
args
);
}

@Override
public CompletableFuture<WorkspaceEdit> resolve(CodeAction codeAction) {
if (codeAction.getData() instanceof JsonElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;


import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
Expand Down Expand Up @@ -41,10 +40,13 @@
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import org.springframework.ide.vscode.commons.languageserver.util.Settings;

@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
Expand Down Expand Up @@ -99,6 +101,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-multiline", true)
)))));

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

Expand All @@ -114,6 +122,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("\nSELECT"), "Should have newline before SELECT");
assertTrue(queryValue.contains("\nFROM"), "Should have newline before FROM");
assertTrue(queryValue.contains("\nWHERE"), "Should have newline before WHERE");
assertTrue(queryValue.endsWith("\n\"\"\""), "Should end with newline 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 @@ -86,10 +86,8 @@ void convertToQueryCodeAction() throws Exception {
TextDocumentEdit docEdit = edit.getDocumentChanges().get(0).getLeft();
String rawText = docEdit.getEdits().get(0).getNewText();

assertEquals("""
@Query(\"""
SELECT u FROM users u WHERE u.lastname LIKE :lastname ESCAPE '\\' ORDER BY u.firstname asc
\""")""",
assertEquals(
"@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
5 changes: 5 additions & 0 deletions vscode-extensions/vscode-spring-boot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,11 @@
"default": true,
"description": "Show CodeLens with AOT generated query over Data Query methods"
},
"boot-java.code-action.data-query-multiline": {
"type": "boolean",
"default": false,
"description": "Generate @Query annotations as Java text blocks with formatted multi-line layout"
},
"boot-java.java.codelens-web-configs-on-controller-classes": {
"type": "boolean",
"default": true,
Expand Down