From 009ae987005bad00f49794a2b1a16579809d43ae Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Mon, 6 Jul 2020 13:04:13 +0200 Subject: [PATCH] initial version of SpEL syntax validation --- .../boot/ls/BootJavaPreferencesPage.java | 1 + .../tooling/boot/ls/Constants.java | 2 + .../DelegatingStreamConnectionProvider.java | 7 + .../tooling/boot/ls/PrefsInitializer.java | 1 + .../ide/vscode/boot/app/BootJavaConfig.java | 7 + .../BootJavaLanguageServerComponents.java | 14 +- .../handlers/BootJavaReconcileEngine.java | 122 +++++++++++++++++- .../src/browser/boot-preferences.ts | 6 + .../vscode-spring-boot/package.json | 5 + 9 files changed, 161 insertions(+), 4 deletions(-) diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootJavaPreferencesPage.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootJavaPreferencesPage.java index 685c310da..bfc1a294a 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootJavaPreferencesPage.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/BootJavaPreferencesPage.java @@ -97,6 +97,7 @@ public class BootJavaPreferencesPage extends FieldEditorPreferencePage implement }); addField(new BooleanFieldEditor(Constants.PREF_CHANGE_DETECTION, "Live Boot Change Detection", fieldEditorParent)); + addField(new BooleanFieldEditor(Constants.PREF_VALIDATION_SPEL_EXPRESSIONS, "SpEL Expression Syntax Validation", fieldEditorParent)); } } diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/Constants.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/Constants.java index 0346148ee..1e29af2e3 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/Constants.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/Constants.java @@ -25,6 +25,8 @@ public class Constants { public static final String PREF_SCAN_JAVA_TEST_SOURCES = "boot-java.scan-java-test-sources"; + public static final String PREF_VALIDATION_SPEL_EXPRESSIONS = "boot-java.validation.spel.on"; + public static final String PREF_SUPPORT_SPRING_XML_CONFIGS = "boot-java.support-spring-xml-config.on"; public static final String PREF_XML_CONFIGS_SCAN_FOLDERS = "boot-java.support-spring-xml-config.scan-folders-globs"; public static final String PREF_XML_CONFIGS_HYPERLINKS = "boot-java.support-spring-xml-config.hyperlinks"; diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java index 22533fa54..554d4871b 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java @@ -153,6 +153,8 @@ public class DelegatingStreamConnectionProvider implements StreamConnectionProvi Map supportXML = new HashMap<>(); Map bootChangeDetection = new HashMap<>(); Map scanTestJavaSources = new HashMap<>(); + Map validation = new HashMap<>(); + Map validationSpelExpressions = new HashMap<>(); IPreferenceStore preferenceStore = BootLanguageServerPlugin.getDefault().getPreferenceStore(); @@ -172,10 +174,15 @@ public class DelegatingStreamConnectionProvider implements StreamConnectionProvi bootChangeDetection.put("on", preferenceStore.getBoolean(Constants.PREF_CHANGE_DETECTION)); scanTestJavaSources.put("on", preferenceStore.getBoolean(Constants.PREF_SCAN_JAVA_TEST_SOURCES)); + validationSpelExpressions.put("on", preferenceStore.getBoolean(Constants.PREF_VALIDATION_SPEL_EXPRESSIONS)); + validation.put("spel", validationSpelExpressions); + bootJavaObj.put("live-information", liveInformation); bootJavaObj.put("support-spring-xml-config", supportXML); bootJavaObj.put("change-detection", bootChangeDetection); bootJavaObj.put("scan-java-test-sources", scanTestJavaSources); + bootJavaObj.put("change-detection", bootChangeDetection); + bootJavaObj.put("validation", validation); bootJavaObj.put("remote-apps", getAllRemoteApps()); diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/PrefsInitializer.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/PrefsInitializer.java index 521ee9d89..ff24f0064 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/PrefsInitializer.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/PrefsInitializer.java @@ -39,6 +39,7 @@ public class PrefsInitializer extends AbstractPreferenceInitializer { preferenceStore.setDefault(Constants.PREF_XML_CONFIGS_SCAN_FOLDERS, "src/main"); preferenceStore.setDefault(Constants.PREF_CHANGE_DETECTION, false); + preferenceStore.setDefault(Constants.PREF_VALIDATION_SPEL_EXPRESSIONS, true); preferenceStore.setDefault(Constants.PREF_SCAN_JAVA_TEST_SOURCES, false); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java index 051ad2c21..df732ea3c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java @@ -35,6 +35,8 @@ public class BootJavaConfig implements InitializingBean { public static final int LIVE_INFORMATION_FETCH_DATA_RETRY_MAX_NO_DEFAULT = 10; public static final int LIVE_INFORMATION_FETCH_DATA_RETRY_DELAY_IN_SECONDS_DEFAULT = 3; + + public static final boolean VALIDAITON_SPEL_EXPRESSIONS_ENABLED_DEFAULT = true; //TODO: Consider changing this to something that raises Spring application events. @@ -110,6 +112,11 @@ public class BootJavaConfig implements InitializingBean { return enabled != null && enabled.booleanValue(); } + public boolean isSpelExpressionValidationEnabled() { + Boolean enabled = settings.getBoolean("boot-java", "validation", "spel", "on"); + return enabled != null ? enabled.booleanValue() : VALIDAITON_SPEL_EXPRESSIONS_ENABLED_DEFAULT; + } + public boolean areXmlHyperlinksEnabled() { Boolean enabled = settings.getBoolean("boot-java", "support-spring-xml-config", "hyperlinks"); return enabled != null && enabled.booleanValue(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index c11dcc3ca..7f9c1ad78 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.eclipse.lsp4j.CompletionItemKind; @@ -32,6 +33,7 @@ import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngin import org.springframework.ide.vscode.boot.java.handlers.BootJavaDocumentHighlightEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaDocumentSymbolHandler; import org.springframework.ide.vscode.boot.java.handlers.BootJavaHoverProvider; +import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaReferencesHandler; import org.springframework.ide.vscode.boot.java.handlers.BootJavaWorkspaceSymbolHandler; import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider; @@ -70,6 +72,7 @@ import org.springframework.ide.vscode.commons.languageserver.completion.IComplet import org.springframework.ide.vscode.commons.languageserver.composable.LanguageServerComponents; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; import org.springframework.ide.vscode.commons.languageserver.util.CodeLensHandler; import org.springframework.ide.vscode.commons.languageserver.util.DocumentHighlightHandler; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; @@ -103,6 +106,8 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent private final SimpleLanguageServer server; private final BootLanguageServerParams serverParams; + private final BootJavaConfig config; + private final SpringPropertyIndexProvider propertyIndexProvider; private final ProjectBasedPropertyIndexProvider adHocPropertyIndexProvider; @@ -120,7 +125,6 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent private SpringProcessTracker liveProcessTracker; - public BootJavaLanguageServerComponents( SimpleLanguageServer server, BootLanguageServerParams serverParams, @@ -134,6 +138,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent ) { this.server = server; this.serverParams = serverParams; + this.config = config; projectFinder = serverParams.projectFinder; projectObserver = serverParams.projectObserver; @@ -197,7 +202,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent highlightsEngine = createDocumentHighlightEngine(indexer); documents.onDocumentHighlight(highlightsEngine); - + config.addListener(ignore -> { log.info("update live process tracker settings - start"); @@ -245,6 +250,11 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent public DocumentHighlightHandler getDocumentHighlightHandler() { return highlightsEngine; } + + @Override + public Optional getReconcileEngine() { + return Optional.of(new BootJavaReconcileEngine(this, config)); + } private void initialized() { this.liveProcessTracker.start(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReconcileEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReconcileEngine.java index 18959b0dd..08072c6ba 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReconcileEngine.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReconcileEngine.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016-2017 Pivotal, Inc. + * Copyright (c) 2016, 2020 Pivotal, 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 @@ -10,8 +10,30 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.handlers; +import java.net.URI; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.ide.vscode.boot.app.BootJavaConfig; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; +import org.springframework.ide.vscode.boot.java.value.Constants; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemTypes; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; import org.springframework.ide.vscode.commons.util.text.IDocument; /** @@ -19,8 +41,104 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; */ public class BootJavaReconcileEngine implements IReconcileEngine { + private static final Logger log = LoggerFactory.getLogger(BootJavaReconcileEngine.class); + + private final JavaProjectFinder projectFinder; + private final BootJavaLanguageServerComponents server; + private final BootJavaConfig config; + + public BootJavaReconcileEngine(BootJavaLanguageServerComponents server, BootJavaConfig config) { + this.server = server; + this.config = config; + this.projectFinder = this.server.getProjectFinder(); + } + @Override - public void reconcile(IDocument doc, IProblemCollector problemCollector) { + public void reconcile(final IDocument doc, final IProblemCollector problemCollector) { + System.out.println("reconcile document: " + doc); + + IJavaProject project = projectFinder.find(new TextDocumentIdentifier(doc.getUri())).orElse(null); + URI uri = URI.create(doc.getUri()); + + if (project != null) { + + try { + problemCollector.beginCollecting(); + + server.getCompilationUnitCache().withCompilationUnit(project, uri, cu -> { + if (cu != null) { + reconcileAST(cu, problemCollector); + } + + return null; + }); + } + finally { + problemCollector.endCollecting(); + } + } + } + + private void reconcileAST(CompilationUnit cu, IProblemCollector problemCollector) { + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(SingleMemberAnnotation node) { + try { + visitAnnotation(node, problemCollector); + } + catch (Exception e) { + } + return super.visit(node); + } + }); + } + + protected void visitAnnotation(SingleMemberAnnotation node, IProblemCollector problemCollector) { + if (!config.isSpelExpressionValidationEnabled()) { + return; + } + + ITypeBinding typeBinding = node.resolveTypeBinding(); + + if (typeBinding != null) { + String qname = typeBinding.getQualifiedName(); + if (Constants.SPRING_VALUE.equals(qname)) { + Expression valueExp = node.getValue(); + + if (valueExp instanceof StringLiteral) { + String value = ((StringLiteral) valueExp).getLiteralValue(); + if (value != null && value.startsWith("#{") && value.endsWith("}")) { + reconcileSpELExpressionValue(value, valueExp, problemCollector); + } + } + } + } + } + + private void reconcileSpELExpressionValue(String value, Expression valueExp, IProblemCollector problemCollector) { + String spelExpression = value.substring(2, value.length() - 1); + + if (spelExpression.length() > 0) { + SpelExpressionParser parser = new SpelExpressionParser(); + try { + parser.parseExpression(spelExpression); + } + catch (ParseException e) { + String message = e.getSimpleMessage(); + int position = e.getPosition(); + + createProblem(valueExp, message, position, problemCollector); + } + } + } + + private void createProblem(Expression valueExp, String message, int position, IProblemCollector problemCollector) { + int start = valueExp.getStartPosition() + 3 + position; + int length = valueExp.getLength() - 5 - position; + + ReconcileProblem problem = new ReconcileProblemImpl(ProblemTypes.create("SpEL Expression Problem", ProblemSeverity.ERROR), message, start, length); + problemCollector.accept(problem); } } diff --git a/theia-extensions/theia-spring-boot/spring-boot/src/browser/boot-preferences.ts b/theia-extensions/theia-spring-boot/spring-boot/src/browser/boot-preferences.ts index 8adc5d1d1..916fcc928 100644 --- a/theia-extensions/theia-spring-boot/spring-boot/src/browser/boot-preferences.ts +++ b/theia-extensions/theia-spring-boot/spring-boot/src/browser/boot-preferences.ts @@ -70,6 +70,11 @@ export const BootConfigSchema: PreferenceSchema = { description: 'Enable/Disable detecting changes of running Spring Boot applications.', default: false }, + 'boot-java.validation.spel.on': { + type: 'boolean', + description: 'Validation - Validate SpEL Expression Syntax', + default: true + }, 'boot-java.highlight-codelens.on': { type: 'boolean', default: true, @@ -99,6 +104,7 @@ export interface BootConfiguration { 'boot-java.support-spring-xml-config.content-assist': boolean; 'boot-java.support-spring-xml-config.scan-folders': string; 'boot-java.change-detection.on': boolean; + 'boot-java.validation.spel.on': boolean; 'boot-java.highlight-codelens.on': boolean; 'spring-boot.ls.javahome': string; 'spring-boot.ls.vmargs': string; diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index d69adccd1..c6eba8964 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -129,6 +129,11 @@ "default": false, "description": "Enable/Disable detecting changes of running Spring Boot applications" }, + "boot-java.validation.spel.on": { + "type": "boolean", + "default": false, + "description": "Validation - Validate SpEL Expression Syntax" + }, "boot-java.remote-apps": { "type": "array", "items": {