Skip to content
Merged
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 @@ -41,6 +41,7 @@ public static class ProblemParameterData {
private String description;
private String type;
private String defaultValue;
private String[] enumValues;

public ProblemParameterData() {}

Expand Down Expand Up @@ -83,6 +84,14 @@ public String getDefaultValue() {
public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}

public String[] getEnumValues() {
return enumValues;
}

public void setEnumValues(String[] enumValues) {
this.enumValues = enumValues;
}
}

public static class ProblemTypeData implements ProblemType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@
class="org.springframework.tooling.boot.ls.commands.ConvertBootPropertiesHanlder$ConvertPropertiesToYamlHandler"
commandId="org.springframework.tooling.boot.ls.properties.convert-props-to-yaml">
</handler>
<handler
class="org.springframework.tooling.boot.ls.commands.SetConfigurationHandler"
commandId="boot-ls.client.set-configuration">
</handler>
</extension>
<extension
point="org.eclipse.ui.commands">
Expand Down Expand Up @@ -322,6 +326,24 @@
typeId="org.eclipse.lsp4e.pathParameterType">
</commandParameter>
</command>
<command
categoryId="org.eclipse.lsp4e.commandCategory"
description="Sets a configuration value, as sent by a language server quick fix"
id="boot-ls.client.set-configuration"
name="Set Configuration">
<commandParameter
id="org.eclipse.lsp4e.command.param"
name="Command id (unnecessary, only to make lsp4e happy)"
optional="true"
typeId="org.eclipse.lsp4e.commandParameterType">
</commandParameter>
<commandParameter
id="org.eclipse.lsp4e.path.param"
name="Resource Path (unnecessary, only to make lsp4e happy)"
optional="true"
typeId="org.eclipse.lsp4e.pathParameterType">
</commandParameter>
</command>
<command
description="Converts Spring Boot properties file from .properties format to .yaml format"
id="org.springframework.tooling.boot.ls.properties.convert-yaml-to-props"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*******************************************************************************
* 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.tooling.boot.ls.commands;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.lsp4e.command.LSPCommandHandler;
import org.eclipse.lsp4j.Command;
import org.springframework.tooling.boot.ls.BootLanguageServerPlugin;

import com.google.gson.Gson;

/**
* Handles {@code boot-ls.client.set-configuration} - a generic "set a
* configuration value" command the language server invokes from quick fixes
* (e.g. the SQL dialect quick fix), reusable for future settings without a
* dedicated command/handler pair per setting. Always writes to the plugin's
* own preference store (no per-resource scope is supported); the
* preference-change listener already registered in
* {@code DelegatingStreamConnectionProvider} picks up the change and
* re-sends the whole configuration to the language server, same as any
* other preference change.
* <p>
* The key is the language server's own dotted setting name (as sent to it
* over {@code workspace/didChangeConfiguration}). Settings relayed through
* the {@code spring-boot.ls.problem}/{@code spring-boot.ls.problem-parameters}
* mechanism (see {@code DelegatingStreamConnectionProvider}) are stored
* locally without that prefix, so it's stripped here to get the actual
* preference key.
*/
@SuppressWarnings("restriction")
public class SetConfigurationHandler extends AbstractHandler {

private static final String PROBLEM_SETTINGS_PREFIX = "spring-boot.ls.";

@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
try {
String p = event.getParameter(LSPCommandHandler.LSP_COMMAND_PARAMETER_ID);
Command cmd = new Gson().fromJson(p, Command.class);
if (cmd != null && cmd.getArguments() != null && cmd.getArguments().size() >= 2) {
String key = cmd.getArguments().get(0).toString();
String value = cmd.getArguments().get(1).toString();
String prefKey = key.startsWith(PROBLEM_SETTINGS_PREFIX) ? key.substring(PROBLEM_SETTINGS_PREFIX.length()) : key;
IPreferenceStore preferenceStore = BootLanguageServerPlugin.getDefault().getPreferenceStore();
preferenceStore.setValue(prefKey, value);
}
return null;
} catch (Exception e) {
throw new ExecutionException("Failed to execute Set Configuration command", e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ protected void initializeDefaults() {
defaults.put(getProblemParametersPreferencePrefix() + param.getKey(), param.getDefaultValue());
}
}
super.initializeDefaults();
super.initializeDefaults();
}

@Override
protected void createFieldEditors() {
if (category.getToggle() != null) {
Expand All @@ -84,7 +84,9 @@ protected void createFieldEditors() {
if (category.getParameters() != null) {
for (ProblemParameterData param : category.getParameters()) {
String prefKey = getProblemParametersPreferencePrefix() + param.getKey();
if ("boolean".equals(param.getType())) {
if (param.getEnumValues() != null) {
addField(new ComboFieldEditor(prefKey, param.getLabel(), createToggleValues(param.getEnumValues()), getFieldEditorParent()));
} else if ("boolean".equals(param.getType())) {
addField(new BooleanFieldEditor(prefKey, param.getLabel(), getFieldEditorParent()));
} else if ("integer".equals(param.getType())) {
addField(new IntegerFieldEditor(prefKey, param.getLabel(), getFieldEditorParent()));
Expand All @@ -95,7 +97,7 @@ protected void createFieldEditors() {
}
super.createFieldEditors();
}

private static String[][] createToggleValues(String[] values) {
String[][] res = new String[values.length][2];
for (int i = 0; i < values.length; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,24 @@ public enum ValueType {
private final String description;
private final ValueType type;
private final String defaultValue;
private final String[] enumValues;

public ProblemTypeParameter(String key, String label, String description, ValueType type, String defaultValue) {
this(key, label, description, type, defaultValue, null);
}

/**
* @param enumValues when non-null, this parameter is rendered as a fixed
* choice among these raw values, rather than a free-form field for {@code type}
*/
public ProblemTypeParameter(String key, String label, String description, ValueType type, String defaultValue,
String[] enumValues) {
this.key = key;
this.label = label;
this.description = description;
this.type = type;
this.defaultValue = defaultValue;
this.enumValues = enumValues;
}

public String getKey() {
Expand All @@ -56,4 +67,8 @@ public String getDefaultValue() {
return defaultValue;
}

public String[] getEnumValues() {
return enumValues;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ public void addListener(Consumer<Void> l) {
listeners.add(l);
}

/**
* The global override for the SQL dialect used to validate native
* {@code @Query} statements: {@code auto} (default), {@code mysql}, or
* {@code postgresql}. See {@code SqlDialectResolver}.
*/
public String getSqlDialect() {
return settings.getString("spring-boot", "ls", "problem-parameters", "data-query", "sql-dialect");
}

@Override
public void afterPropertiesSet() throws Exception {
settingsStore.onDidChange(this::handleConfigurationChange);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JpqlSemanticTokens;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JpqlSupportState;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.QueryJdtAstReconciler;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.SqlDialectResolver;
import org.springframework.ide.vscode.boot.java.handlers.Reconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.AddConfigurationIfBeansPresentReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.ApplicationModuleListenerReconciler;
Expand Down Expand Up @@ -263,8 +264,9 @@ public class JdtConfig {
@Bean QueryJdtAstReconciler dataQueryReconciler(
@Qualifier("hqlReconciler") Reconciler hqlReconciler,
@Qualifier("jpqlReconciler") Reconciler jpqlReconciler,
Optional<SpelReconciler> spelReconciler) {
return new QueryJdtAstReconciler(hqlReconciler, jpqlReconciler, spelReconciler);
Optional<SpelReconciler> spelReconciler,
SqlDialectResolver sqlDialectResolver) {
return new QueryJdtAstReconciler(hqlReconciler, jpqlReconciler, spelReconciler, sqlDialectResolver);
}

@Bean EmbeddedLanguagesSemanticTokensSupport embbededLanguagesSyntaxHighlighting(SimpleLanguageServer server, BootJavaConfig config) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, Inc.
* Copyright (c) 2024, 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
Expand All @@ -18,6 +18,8 @@
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JpqlSemanticTokens;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JpqlSupportState;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.QueryProblemType;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.SqlDialectQuickFixProvider;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.SqlDialectResolver;
import org.springframework.ide.vscode.boot.java.embedded.lang.AntlrReconcilerWithSpel;
import org.springframework.ide.vscode.boot.java.spel.SpelReconciler;
import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens;
Expand Down Expand Up @@ -55,5 +57,15 @@ AntlrReconcilerWithSpel jpqlReconciler(Optional<SpelReconciler> spelReconciler)
JpqlSupportState jpqlSupportState(SimpleLanguageServer server, ProjectObserver projectObserver, BootJavaConfig config) {
return new JpqlSupportState(server, projectObserver, config);
}


@Bean
SqlDialectResolver sqlDialectResolver(BootJavaConfig config) {
return new SqlDialectResolver(config);
}

@Bean
SqlDialectQuickFixProvider sqlDialectQuickFixProvider(SqlDialectResolver sqlDialectResolver) {
return new SqlDialectQuickFixProvider(sqlDialectResolver);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022, 2025 VMware, Inc.
* Copyright (c) 2022, 2026 VMware, 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
Expand All @@ -20,7 +20,21 @@
import static org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemCategory.Toggle.Option.*;

public class SpringProblemCategories {


private static final String PROBLEM_PARAMETERS_SETTING_PREFIX = "spring-boot.ls.problem-parameters";

/**
* The full, dotted setting name a {@code category}'s parameter is read
* from - the same {@code spring-boot.ls.problem-parameters.<category>.<key>}
* path {@code ProblemTypesToJson} and Eclipse's {@code DelegatingStreamConnectionProvider}
* already derive independently, computed here instead of duplicated as a
* literal, so a quick fix can reference a parameter's setting name without
* hardcoding it.
*/
public static String problemParameterSettingKey(ProblemCategory category, String parameterKey) {
return PROBLEM_PARAMETERS_SETTING_PREFIX + "." + category.getId() + "." + parameterKey;
}

public static final ProblemCategory BOOT_2 = new ProblemCategory("boot2", "Boot 2.x Best Practices & Optimizations",
new Toggle("Enablement", EnumSet.allOf(Toggle.Option.class), AUTO, "boot-java.validation.java.boot2"));

Expand All @@ -45,7 +59,11 @@ public class SpringProblemCategories {
List.of(new ProblemTypeParameter("use-project-build-file", "Check project repositories for available versions", "When enabled, uses the Maven repositories configured in the project build file to look up available Spring Boot versions. Falls back to spring.io if the repositories cannot be queried.", ProblemTypeParameter.ValueType.BOOLEAN, "true")));

public static final ProblemCategory DATA_QUERY = new ProblemCategory("data-query", "Data Queries",
new Toggle("Enablement", EnumSet.of(OFF, ON), ON, "boot-java.validation.data-query"));
new Toggle("Enablement", EnumSet.of(OFF, ON), ON, "boot-java.validation.data-query"),
List.of(new ProblemTypeParameter("sql-dialect", "SQL Dialect",
"Overrides the SQL dialect used to validate native @Query SQL statements. By default the dialect is inferred from JDBC driver dependencies (MySQL/MariaDB take precedence over PostgreSQL when both are present).",
ProblemTypeParameter.ValueType.STRING, "auto",
new String[] { "auto", "mysql", "postgresql" })));

public static final ProblemCategory CRON = new ProblemCategory("cron", "CRON Expressions",
new Toggle("Enablement", EnumSet.of(OFF, ON), ON, "boot-java.validation.cron"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ public class QueryJdtAstReconciler implements JdtAstReconciler {
private final Reconciler hqlReconciler;
private final Reconciler jpqlReconciler;
private final Map<SqlType, Reconciler> sqlReconcilers;
private final SqlDialectResolver sqlDialectResolver;



public QueryJdtAstReconciler(Reconciler hqlReconciler, Reconciler jpqlReconciler,
Optional<SpelReconciler> spelReconciler) {
Optional<SpelReconciler> spelReconciler, SqlDialectResolver sqlDialectResolver) {
this.hqlReconciler = hqlReconciler;
this.jpqlReconciler = jpqlReconciler;

this.sqlDialectResolver = sqlDialectResolver;

this.sqlReconcilers = new LinkedHashMap<>();
this.sqlReconcilers.put(SqlType.MYSQL, new AntlrReconcilerWithSpel("MySQL", MySqlParser.class, MySqlLexer.class, "sqlStatements", QueryProblemType.SQL_SYNTAX, spelReconciler, MySqlLexer.SPEL));
this.sqlReconcilers.put(SqlType.POSTGRESQL, new AntlrReconcilerWithSpel("PostgreSQL", PostgreSqlParser.class, PostgreSqlLexer.class, "root", QueryProblemType.SQL_SYNTAX, spelReconciler, PostgreSqlLexer.SPEL));
Expand All @@ -62,8 +64,7 @@ public Optional<ASTVisitor> createVisitor(IJavaProject project, URI docURI, Comp
public boolean visit(NormalAnnotation node) {
EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, node);
if (q != null) {
Optional<Reconciler> reconcilerOpt = q.isNative() ? getSqlReconciler(project) : Optional.of(getQueryReconciler(project));
reconcilerOpt.ifPresent(r -> r.reconcile(q.query().getText(), q.query()::toSingleJavaRange, context.getProblemCollector()));
reconcileQuery(project, q, context);
}
return super.visit(node);
}
Expand All @@ -72,8 +73,7 @@ public boolean visit(NormalAnnotation node) {
public boolean visit(SingleMemberAnnotation node) {
EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, node);
if (q != null) {
Optional<Reconciler> reconcilerOpt = q.isNative() ? getSqlReconciler(project) : Optional.of(getQueryReconciler(project));
reconcilerOpt.ifPresent(r -> r.reconcile(q.query().getText(), q.query()::toSingleJavaRange, context.getProblemCollector()));
reconcileQuery(project, q, context);
}
return super.visit(node);
}
Expand All @@ -96,7 +96,19 @@ public boolean visit(MethodInvocation node) {
private Reconciler getQueryReconciler(IJavaProject project) {
return SpringProjectUtil.hasDependencyStartingWith(project, "hibernate-core", null) ? hqlReconciler : jpqlReconciler;
}


private void reconcileQuery(IJavaProject project, EmbeddedQueryExpression q, ReconcilingContext context) {
if (q.isNative()) {
SqlType resolved = sqlDialectResolver.resolve(project);
if (resolved == null) {
return;
}
sqlReconcilers.get(resolved).reconcile(q.query().getText(), q.query()::toSingleJavaRange, context.getProblemCollector());
} else {
getQueryReconciler(project).reconcile(q.query().getText(), q.query()::toSingleJavaRange, context.getProblemCollector());
}
}

// public static void reconcileExpression(Reconciler reconciler, Expression valueExp, IProblemCollector problemCollector) {
// String query = null;
// int offset = 0;
Expand Down Expand Up @@ -125,18 +137,5 @@ public boolean isApplicable(IJavaProject project) {
public ProblemType getProblemType() {
return QueryProblemType.JPQL_SYNTAX;
}

private Optional<Reconciler> getSqlReconciler(IJavaProject project) {
if (SpringProjectUtil.hasDependencyStartingWith(project, "mysql-connector", null)
|| SpringProjectUtil.hasDependencyStartingWith(project, "mariadb-java-client", null)) {
return Optional.of(sqlReconcilers.get(SqlType.MYSQL));
} else if (SpringProjectUtil.hasDependencyStartingWith(project, "postgresql", null)) {
return Optional.of(sqlReconcilers.get(SqlType.POSTGRESQL));
} else if (SpringProjectUtil.hasDependencyStartingWith(project, "h2", null)) {
// Keep H2 the last as it might be added in combination with other DB clients
return Optional.of(sqlReconcilers.get(SqlType.POSTGRESQL));
}
return Optional.empty();
}

}
Loading
Loading