diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java index 2259726ea..01ce086ce 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java @@ -57,8 +57,8 @@ public final class AnnotationParamSpelExtractor { new AnnotationParamSpelExtractor(SPRING_POST_FILTER, null, "", ""), new AnnotationParamSpelExtractor(SPRING_POST_FILTER, "value", "", ""), - new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, null, "", ""), - new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "", ""), + new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, null, "#{", "}", true), + new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "#{", "}", true), new AnnotationParamSpelExtractor(Annotations.SCHEDULED, "cron", "#{", "}"), }; @@ -66,19 +66,27 @@ public final class AnnotationParamSpelExtractor { public record Snippet(String text, int offset) {} + public record PrefixSuffix(String prefix, String suffix) {} + private final String annotationType; private final String paramName; - private final String paramValuePrefix; - private final String paramValuePostfix; + + private final List prefixSuffixes; public AnnotationParamSpelExtractor(String annotationType, String paramName, String paramValuePrefix, - String paramValuePostfix) { + String paramValueSuffix) { this.annotationType = annotationType; this.paramName = paramName; - this.paramValuePrefix = paramValuePrefix; - this.paramValuePostfix = paramValuePostfix; + this.prefixSuffixes = List.of(new PrefixSuffix(paramValuePrefix, paramValueSuffix)); } + public AnnotationParamSpelExtractor(String annotationType, String paramName, String paramValuePrefix, + String paramValueSuffx, boolean optinalPrefixAndSuffix) { + this.annotationType = annotationType; + this.paramName = paramName; + this.prefixSuffixes = List.of(new PrefixSuffix(paramValuePrefix, paramValueSuffx), new PrefixSuffix("", "")); + } + public Optional getSpelRegion(NormalAnnotation a) { if (paramName == null) { return Optional.empty(); @@ -128,13 +136,15 @@ public final class AnnotationParamSpelExtractor { String value = valueExp.getEscapedValue(); value = value.substring(1, value.length() - 1); if (value != null) { - int startIdx = value.indexOf(paramValuePrefix); - if (startIdx >= 0) { - int endIdx = value.lastIndexOf(paramValuePostfix); - if (endIdx >= 0) { - String spelText = value.substring(startIdx + paramValuePrefix.length(), endIdx); - int offset = valueExp.getStartPosition() + startIdx + paramValuePrefix.length() + 1; - return Optional.of(new Snippet(spelText, offset)); + for (PrefixSuffix ps : prefixSuffixes) { + int startIdx = value.indexOf(ps.prefix); + if (startIdx >= 0) { + int endIdx = value.lastIndexOf(ps.suffix); + if (endIdx >= 0) { + String spelText = value.substring(startIdx + ps.prefix.length(), endIdx); + int offset = valueExp.getStartPosition() + startIdx + ps.prefix.length() + 1; + return Optional.of(new Snippet(spelText, offset)); + } } } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/spel/JdtSpelReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/spel/JdtSpelReconcilerTest.java new file mode 100644 index 000000000..ba265318c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/spel/JdtSpelReconcilerTest.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 2024 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.spel; + +import java.io.File; + +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.Settings; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +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.JsonElement; + +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class JdtSpelReconcilerTest { + + @Autowired + private BootLanguageServerHarness harness; + @Autowired + private JavaProjectFinder projectFinder; + + private File directory; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/boot-mysql/").toURI()); + + String projectDir = directory.toURI().toString(); + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + String changedSettings = """ + { + "spring-boot": { + "ls": { + "problem": { + "spel": { + "JAVA_SPEL_EXPRESSION_SYNTAX": "ERROR" + } + } + } + } + } + """; + JsonElement settingsAsJson = new Gson().fromJson(changedSettings, JsonElement.class); + harness.changeConfiguration(new Settings(settingsAsJson)); + + } + + @Test + void noErrors_ConditionalOnExpression_1() throws Exception { + String source = """ + package example.demo; + + import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + + @ConditionalOnExpression("${server.port:8080} == 5678") + public class A { + } + """; + String docUri = directory.toPath().resolve("src/main/java/example/demo/A.java").toUri() + .toString(); + Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri); + editor.assertProblems(); + } + + @Test + void noErrors_ConditionalOnExpression_2() throws Exception { + String source = """ + package example.demo; + + import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + + @ConditionalOnExpression("#{${server.port:8080} == 5678}") + public class A { + } + """; + String docUri = directory.toPath().resolve("src/main/java/example/demo/A.java").toUri() + .toString(); + Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri); + editor.assertProblems(); + } + + @Test + void errors_ConditionalOnExpression_1() throws Exception { + String source = """ + package example.demo; + + import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + + @ConditionalOnExpression("${server.port:8080 == 5678") + public class A { + } + """; + String docUri = directory.toPath().resolve("src/main/java/example/demo/A.java").toUri() + .toString(); + Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri); + editor.assertProblems("\"|SPEL: mismatched input ''"); + } + + @Test + void errors_ConditionalOnExpression_2() throws Exception { + String source = """ + package example.demo; + + import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + + @ConditionalOnExpression("#{${server.port:8080 == 5678}") + public class A { + } + """; + String docUri = directory.toPath().resolve("src/main/java/example/demo/A.java").toUri() + .toString(); + Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri); + editor.assertProblems("}|SPEL: mismatched input ''"); + } +}