Skip to content

Commit af24404

Browse files
authored
Add support for multiline query formatting in Query annotations (#1763)
* Use Java text blocks for query conversion in Java 15+ Signed-off-by: Junhwan Kim <musoyou1085@gmail.com> * refactor use Java text blocks when generating Query annotations Signed-off-by: Junhwan Kim <musoyou1085@gmail.com> * Add support for multiline query formatting in query annotations - Introduce QueryFormatter interface and implementations for JPQL and MongoDB queries - Add BootJavaConfig option to select query style (compact or multiline) - Update code lens and code action providers to use configurable query formatting - Enhance tests to verify multiline and compact query styles Signed-off-by: Junhwan Kim <musoyou1085@gmail.com> * Refactor multiline query formatting to remove indentation and update related tests - Remove extra indentation from multiline JPQL and Mongo query formatting - Adjust test assertions to match new formatting without leading spaces - Update text block generation to align closing triple quotes without indentation - Add configuration option for data query style in package.json - Enhance code action provider to include fix data for refactorings Signed-off-by: Junhwan Kim <musoyou1085@gmail.com> * Add preference for data query style with compact and multiline options Signed-off-by: Junhwan Kim <musoyou1085@gmail.com> * Refactor data query style handling to support multiline text blocks Signed-off-by: Junhwan Kim <musoyou1085@gmail.com> --------- Signed-off-by: Junhwan Kim <musoyou1085@gmail.com>
1 parent 9af44dc commit af24404

File tree

13 files changed

+101
-27
lines changed

13 files changed

+101
-27
lines changed

eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/Constants.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ public class Constants {
5959
public static final String PREF_CODELENS_QUERY_METHODS = "boot-java.java.codelens-over-query-methods";
6060

6161
public static final String PREF_CODELENS_WEB_CONFIGS_ON_CONTROLLER_CLASSES = "boot-java.java.codelens-web-configs-on-controller-classes";
62-
62+
63+
public static final String PREF_DATA_QUERY_MULTILINE = "boot-java.code-action.data-query-multiline";
64+
6365
public static final String PREF_AI_MCP_ENABLED = "boot-java.ai.mcp-server-enabled";
6466
public static final String PREF_AI_MCP_PORT = "boot-java.ai.mcp-server-port";
6567

eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ private void sendConfiguration() {
237237
bootJavaObj.put("java", javaSettings);
238238
bootJavaObj.put("remote-apps", getAllRemoteApps());
239239
bootJavaObj.put("modulith-project-tracking", preferenceStore.getBoolean(Constants.PREF_MODULITH));
240+
bootJavaObj.put("code-action", Map.of("data-query-multiline", preferenceStore.getBoolean(Constants.PREF_DATA_QUERY_MULTILINE)));
240241

241242
bootJavaObj.put("rewrite", Map.of(
242243
"recipe-filters", StringListEditor.decode(preferenceStore.getString(Constants.PREF_REWRITE_RECIPE_FILTERS)),

eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/BootJavaPreferencesPage.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ protected void createFieldEditors() {
5858

5959
// Show AOT generated query over Data Query methods
6060
addField(new BooleanFieldEditor(Constants.PREF_CODELENS_QUERY_METHODS, "Show CodeLens with AOT generated query over Data Query methods", fieldEditorParent));
61+
62+
// Data Query Multiline
63+
addField(new BooleanFieldEditor(Constants.PREF_DATA_QUERY_MULTILINE, "Generate @Query as multiline text block", fieldEditorParent));
6164

6265
// Show Web Config code lens
6366
addField(new BooleanFieldEditor(Constants.PREF_CODELENS_WEB_CONFIGS_ON_CONTROLLER_CLASSES, "Show CodeLens with Web Config Details for controllers", fieldEditorParent));

eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/PrefsInitializer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ public void initializeDefaultPreferences() {
5454
preferenceStore.setDefault(Constants.PREF_SYMBOLS_FROM_NEW_INDEX, true);
5555
preferenceStore.setDefault(Constants.PREF_CODELENS_QUERY_METHODS, true);
5656
preferenceStore.setDefault(Constants.PREF_CODELENS_WEB_CONFIGS_ON_CONTROLLER_CLASSES, true);
57-
57+
preferenceStore.setDefault(Constants.PREF_DATA_QUERY_MULTILINE, false);
58+
5859
preferenceStore.setDefault(Constants.PREF_AI_MCP_ENABLED, false);
5960
preferenceStore.setDefault(Constants.PREF_AI_MCP_PORT, 50627);
6061
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ public boolean isEnabledCodeLensOverDataQueryMethods() {
229229
return Boolean.TRUE.equals(b);
230230
}
231231

232+
public boolean isDataQueryMultiline() {
233+
Boolean b = settings.getBoolean("boot-java", "code-action", "data-query-multiline");
234+
return Boolean.TRUE.equals(b);
235+
}
236+
232237
public boolean isEnabledCodeLensForWebConfigs() {
233238
Boolean b = settings.getBoolean("boot-java", "java", "codelens-web-configs-on-controller-classes");
234239
return Boolean.TRUE.equals(b);

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/RewriteConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ public class RewriteConfig {
4646
}
4747

4848
@ConditionalOnBean(RewriteRefactorings.class)
49-
@Bean QueryMethodCodeActionProvider queryMethodCodeActionProvider(DataRepositoryAotMetadataService dataRepoAotService, RewriteRefactorings refactorings) {
50-
return new QueryMethodCodeActionProvider(dataRepoAotService, refactorings);
49+
@Bean QueryMethodCodeActionProvider queryMethodCodeActionProvider(DataRepositoryAotMetadataService dataRepoAotService, RewriteRefactorings refactorings, BootJavaConfig config) {
50+
return new QueryMethodCodeActionProvider(dataRepoAotService, refactorings, config);
5151
}
5252

5353
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataCodeLensProvider.java

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Set;
1919
import java.util.stream.Collectors;
2020

21+
import org.apache.commons.text.StringEscapeUtils;
2122
import org.eclipse.jdt.core.dom.ASTVisitor;
2223
import org.eclipse.jdt.core.dom.CompilationUnit;
2324
import org.eclipse.jdt.core.dom.IMethodBinding;
@@ -27,12 +28,15 @@
2728
import org.eclipse.lsp4j.Position;
2829
import org.eclipse.lsp4j.Range;
2930
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
31+
import org.json.JSONObject;
3032
import org.slf4j.Logger;
3133
import org.slf4j.LoggerFactory;
3234
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
3335
import org.springframework.ide.vscode.boot.java.Annotations;
3436
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
3537
import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider;
38+
import org.springframework.ide.vscode.parser.hql.HqlQueryFormatter;
39+
import org.springframework.ide.vscode.parser.postgresql.PostgreSqlQueryFormatter;
3640
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
3741
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
3842
import org.springframework.ide.vscode.commons.java.IJavaProject;
@@ -56,7 +60,7 @@ public class DataRepositoryAotMetadataCodeLensProvider implements CodeLensProvid
5660
DataRepositoryModule.JDBC, Annotations.DATA_JDBC_QUERY,
5761
DataRepositoryModule.MONGODB, Annotations.DATA_MONGODB_QUERY
5862
);
59-
63+
6064
private static final Logger log = LoggerFactory.getLogger(DataRepositoryAotMetadataCodeLensProvider.class);
6165

6266
private final DataRepositoryAotMetadataService repositoryMetadataService;
@@ -155,7 +159,7 @@ private List<CodeLens> createCodeLenses(IJavaProject project, MethodDeclaration
155159
|| hierarchyAnnot.isAnnotatedWith(mb, Annotations.DATA_JDBC_QUERY);
156160

157161
if (!isQueryAnnotated) {
158-
codeLenses.add(new CodeLens(range, refactorings.createFixCommand(COVERT_TO_QUERY_LABEL, createFixDescriptor(mb, document.getUri(), metadata.module(), methodMetadata)), null));
162+
codeLenses.add(new CodeLens(range, refactorings.createFixCommand(COVERT_TO_QUERY_LABEL, createFixDescriptor(mb, document.getUri(), metadata.module(), methodMetadata, config)), null));
159163
}
160164

161165
Command impl = new Command("Go To Implementation", GenAotQueryMethodImplProvider.CMD_NAVIGATE_TO_IMPL, List.of(new GenAotQueryMethodImplProvider.GoToImplParams(
@@ -196,7 +200,7 @@ private Optional<CodeLens> createRefreshCodeLens(IJavaProject project, String ti
196200
});
197201
}
198202

199-
static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataRepositoryModule module, IDataRepositoryAotMethodMetadata methodMetadata) {
203+
static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataRepositoryModule module, IDataRepositoryAotMethodMetadata methodMetadata, BootJavaConfig config) {
200204
return new FixDescriptor(AddAnnotationOverMethod.class.getName(), List.of(docUri), "Turn into `@Query`")
201205
.withRecipeScope(RecipeScope.FILE)
202206
.withParameters(Map.of(
@@ -205,19 +209,37 @@ static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataR
205209
Arrays.stream(mb.getParameterTypes())
206210
.map(pt -> pt.getQualifiedName())
207211
.collect(Collectors.joining(", "))),
208-
"attributes", createAttributeList(methodMetadata.getAttributesMap())));
212+
"attributes", createAttributeList(methodMetadata.getAttributesMap(), module, config)));
209213
}
210214

211-
private static List<AddAnnotationOverMethod.Attribute> createAttributeList(Map<String, String> attributes) {
215+
private static List<AddAnnotationOverMethod.Attribute> createAttributeList(Map<String, String> attributes, DataRepositoryModule module, BootJavaConfig config) {
212216
List<AddAnnotationOverMethod.Attribute> result = new ArrayList<>();
217+
boolean isMultiline = config.isDataQueryMultiline();
218+
213219
for (Map.Entry<String, String> entry : attributes.entrySet()) {
214220
String key = entry.getKey();
215221
String value = entry.getValue();
216-
if (value == null) continue;
217222

218-
result.add(new AddAnnotationOverMethod.Attribute(key, "\"\"\"\n" + value + "\n\"\"\""));
223+
if (isMultiline) {
224+
String formattedValue = switch (module) {
225+
case JPA -> new HqlQueryFormatter().format(value);
226+
case JDBC -> new PostgreSqlQueryFormatter().format(value);
227+
case MONGODB -> {
228+
try {
229+
yield new JSONObject(value).toString(4);
230+
} catch (Exception e) {
231+
yield value;
232+
}
233+
}
234+
};
235+
value = "\"\"\"\n" + formattedValue + "\n\"\"\"";
236+
} else {
237+
value = "\"" + StringEscapeUtils.escapeJava(value).trim() + "\"";
238+
}
239+
240+
result.add(new AddAnnotationOverMethod.Attribute(key, value));
219241
}
220242
return result;
221243
}
222-
223244
}
245+

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import org.eclipse.jdt.core.dom.MethodDeclaration;
1919
import org.eclipse.lsp4j.CodeAction;
2020
import org.eclipse.lsp4j.CodeActionKind;
21+
import org.eclipse.lsp4j.Command;
2122
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
23+
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
2224
import org.springframework.ide.vscode.boot.java.Annotations;
2325
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
2426
import org.springframework.ide.vscode.boot.java.codeaction.JdtAstCodeActionProvider;
@@ -36,10 +38,12 @@ public class QueryMethodCodeActionProvider implements JdtAstCodeActionProvider {
3638

3739
private final DataRepositoryAotMetadataService repositoryMetadataService;
3840
private final RewriteRefactorings refactorings;
41+
private final BootJavaConfig config;
3942

40-
public QueryMethodCodeActionProvider(DataRepositoryAotMetadataService repositoryMetadataService, RewriteRefactorings refactorings) {
43+
public QueryMethodCodeActionProvider(DataRepositoryAotMetadataService repositoryMetadataService, RewriteRefactorings refactorings, BootJavaConfig config) {
4144
this.repositoryMetadataService = repositoryMetadataService;
4245
this.refactorings = refactorings;
46+
this.config = config;
4347
}
4448

4549
@Override
@@ -94,7 +98,7 @@ public boolean visit(MethodDeclaration node) {
9498

9599
private CodeAction createCodeAction(IMethodBinding mb, URI docUri, DataRepositoryAotMetadata metadata, IDataRepositoryAotMethodMetadata method) {
96100
CodeAction ca = new CodeAction();
97-
ca.setCommand(refactorings.createFixCommand(TITLE, DataRepositoryAotMetadataCodeLensProvider.createFixDescriptor(mb, docUri.toASCIIString(), metadata.module(), method)));
101+
ca.setCommand(refactorings.createFixCommand(TITLE, DataRepositoryAotMetadataCodeLensProvider.createFixDescriptor(mb, docUri.toASCIIString(), metadata.module(), method, config)));
98102
ca.setTitle(TITLE);
99103
ca.setKind(CodeActionKind.Refactor);
100104
return ca;

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public Command createFixCommand(String title, FixDescriptor f) {
9292
args
9393
);
9494
}
95-
95+
9696
@Override
9797
public CompletableFuture<WorkspaceEdit> resolve(CodeAction codeAction) {
9898
if (codeAction.getData() instanceof JsonElement) {

headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJdbcTest.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@
1010
*******************************************************************************/
1111
package org.springframework.ide.vscode.boot.java.data.test;
1212

13-
import static org.junit.jupiter.api.Assertions.assertEquals;
14-
import static org.junit.jupiter.api.Assertions.assertNotNull;
15-
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
import static org.junit.jupiter.api.Assertions.*;
14+
1615

1716
import java.nio.charset.StandardCharsets;
1817
import java.nio.file.Files;
@@ -41,10 +40,13 @@
4140
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
4241
import org.springframework.test.context.junit.jupiter.SpringExtension;
4342

43+
import com.google.gson.Gson;
4444
import com.google.gson.JsonArray;
4545
import com.google.gson.JsonElement;
4646
import com.google.gson.JsonObject;
4747

48+
import org.springframework.ide.vscode.commons.languageserver.util.Settings;
49+
4850
@ExtendWith(SpringExtension.class)
4951
@BootLanguageServerTest
5052
@Import(SymbolProviderTestConf.class)
@@ -99,6 +101,12 @@ void noCodeLensOverMethodWithQueryAnnotation() throws Exception {
99101

100102
@Test
101103
void turnIntoQueryUsesTextBlock() throws Exception {
104+
harness.changeConfiguration(new Settings(new Gson()
105+
.toJsonTree(Map.of("boot-java", Map.of(
106+
"java", Map.of("codelens-over-query-methods", true),
107+
"code-action", Map.of("data-query-multiline", true)
108+
)))));
109+
102110
Path filePath = Paths.get(testProject.getLocationUri())
103111
.resolve("src/main/java/example/springdata/aot/CategoryRepository.java");
104112

@@ -114,6 +122,34 @@ void turnIntoQueryUsesTextBlock() throws Exception {
114122
assertNotNull(queryValue, "Query value should not be null");
115123

116124
assertTrue(queryValue.startsWith("\"\"\""), "Query should be generated as a text block");
125+
assertTrue(queryValue.contains("\n"), "Query should be split into multiple lines");
126+
assertTrue(queryValue.contains("\nSELECT"), "Should have newline before SELECT");
127+
assertTrue(queryValue.contains("\nFROM"), "Should have newline before FROM");
128+
assertTrue(queryValue.contains("\nWHERE"), "Should have newline before WHERE");
129+
assertTrue(queryValue.endsWith("\n\"\"\""), "Should end with newline before closing triple quotes");
130+
}
131+
132+
@Test
133+
void turnIntoQueryUsesCompactStyleByDefault() throws Exception {
134+
Path filePath = Paths.get(testProject.getLocationUri())
135+
.resolve("src/main/java/example/springdata/aot/CategoryRepository.java");
136+
137+
Editor editor = harness.newEditor(
138+
LanguageId.JAVA,
139+
new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8),
140+
filePath.toUri().toASCIIString()
141+
);
142+
143+
List<CodeLens> cls = editor.getCodeLenses("findAllByNameContaining", 1);
144+
145+
String queryValue = extractValueFromAttributes(cls.get(0));
146+
147+
System.out.println("Extracted query value: " + queryValue);
148+
assertNotNull(queryValue, "Query value should not be null");
149+
150+
assertTrue(queryValue.startsWith("\""), "Query should be generated as a string literal");
151+
assertFalse(queryValue.startsWith("\"\"\""), "Query should NOT be generated as a text block");
152+
assertFalse(queryValue.contains("\n SELECT"), "Should NOT have formatted newline before SELECT");
117153
}
118154

119155

0 commit comments

Comments
 (0)