diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/AnnotationParamReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/AnnotationParamReconciler.java new file mode 100644 index 000000000..66620aff6 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/AnnotationParamReconciler.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.handlers; + +import java.util.List; + +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; + +/** + * @author Martin Lippert + */ +public class AnnotationParamReconciler { + + private final String annotationType; + private final String paramName; + private final String paramValuePrefix; + private final String paramValuePostfix; + private final Reconciler reconciler; + + public AnnotationParamReconciler(String annotationType, String paramName, + String paramValuePrefix, String paramValuePostfix, Reconciler reconciler) { + this.annotationType = annotationType; + this.paramName = paramName; + this.paramValuePrefix = paramValuePrefix; + this.paramValuePostfix = paramValuePostfix; + this.reconciler = reconciler; + } + + public void visit(SingleMemberAnnotation node, ITypeBinding typeBinding, IProblemCollector problemCollector) { + if (this.paramName != null) { + return; + } + + String qname = typeBinding.getQualifiedName(); + if (!this.annotationType.equals(qname)) { + return; + } + + Expression valueExp = node.getValue(); + + if (valueExp instanceof StringLiteral) { + reconcileStringLiteral((StringLiteral) valueExp, problemCollector); + } + } + + public void visit(NormalAnnotation node, ITypeBinding typeBinding, IProblemCollector problemCollector) { + if (paramName == null) { + return; + } + + String qname = typeBinding.getQualifiedName(); + if (!this.annotationType.equals(qname)) { + return; + } + + List values = node.values(); + + for (Object value : values) { + if (value instanceof MemberValuePair) { + MemberValuePair pair = (MemberValuePair) value; + String name = pair.getName().getFullyQualifiedName(); + if (name != null && name.equals(paramName)) { + Expression expression = pair.getValue(); + if (expression instanceof StringLiteral) { + reconcileStringLiteral((StringLiteral) expression, problemCollector); + } + } + } + } + } + + private void reconcileStringLiteral(StringLiteral valueExp, IProblemCollector problemCollector) { + String value = valueExp.getLiteralValue(); + + if (value != null && value.startsWith(paramValuePrefix) && value.endsWith(paramValuePostfix)) { + String valueToReconcile = value.substring(paramValuePrefix.length(), value.length() - paramValuePostfix.length()); + + reconciler.reconcile(valueToReconcile, valueExp.getStartPosition() + paramValuePrefix.length() + 1, problemCollector); + } + } + +} 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 38f21d181..ff0a725ef 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 @@ -14,25 +14,19 @@ 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.NormalAnnotation; 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.java.utils.CompilationUnitCache; 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.StringUtil; import org.springframework.ide.vscode.commons.util.text.IDocument; /** @@ -40,22 +34,54 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; */ public class BootJavaReconcileEngine implements IReconcileEngine { + // annotations with SpEL expression params + public static final String SPRING_CACHEABLE = "org.springframework.cache.annotation.Cacheable"; + public static final String SPRING_EVENT_LISTENER = "org.springframework.context.event.EventListener"; + + public static final String SPRING_PRE_AUTHORIZE = "org.springframework.security.access.prepost.PreAuthorize"; + public static final String SPRING_PRE_FILTER = "org.springframework.security.access.prepost.PreFilter"; + public static final String SPRING_POST_AUTHORIZE = "org.springframework.security.access.prepost.PostAuthorize"; + public static final String SPRING_POST_FILTER= "org.springframework.security.access.prepost.PostFilter"; + + private static final Logger log = LoggerFactory.getLogger(BootJavaReconcileEngine.class); private final JavaProjectFinder projectFinder; private final CompilationUnitCache compilationUnitCache; - - private boolean spelExpressionValidationEnabled; + private final AnnotationParamReconciler[] reconcilers; + private final SpelExpressionReconciler spelExpressionReconciler; public BootJavaReconcileEngine(CompilationUnitCache compilationUnitCache, JavaProjectFinder projectFinder) { this.compilationUnitCache = compilationUnitCache; this.projectFinder = projectFinder; - this.spelExpressionValidationEnabled = true; + this.spelExpressionReconciler = new SpelExpressionReconciler(); + + this.reconcilers = new AnnotationParamReconciler[] { + + new AnnotationParamReconciler(Constants.SPRING_VALUE, null, "#{", "}", spelExpressionReconciler), + new AnnotationParamReconciler(Constants.SPRING_VALUE, "value", "#{", "}", spelExpressionReconciler), + + new AnnotationParamReconciler(SPRING_CACHEABLE, "key", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_CACHEABLE, "condition", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_CACHEABLE, "unless", "", "", spelExpressionReconciler), + + new AnnotationParamReconciler(SPRING_EVENT_LISTENER, "condition", "", "", spelExpressionReconciler), + + new AnnotationParamReconciler(SPRING_PRE_AUTHORIZE, null, "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_PRE_AUTHORIZE, "value", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_PRE_FILTER, null, "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_PRE_FILTER, "value", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_POST_AUTHORIZE, null, "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_POST_AUTHORIZE, "value", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_POST_FILTER, null, "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_POST_FILTER, "value", "", "", spelExpressionReconciler) + + }; } public void setSpelExpressionSyntaxValidationEnabled(boolean spelExpressionValidationEnabled) { - this.spelExpressionValidationEnabled = spelExpressionValidationEnabled; + this.spelExpressionReconciler.setEnabled(spelExpressionValidationEnabled); } @Override @@ -88,60 +114,44 @@ public class BootJavaReconcileEngine implements IReconcileEngine { @Override public boolean visit(SingleMemberAnnotation node) { try { - visitAnnotation(node, problemCollector); + visitAnnotationWithDefaultParam(node, problemCollector); } catch (Exception e) { } return super.visit(node); } + + @Override + public boolean visit(NormalAnnotation node) { + try { + visitAnnotationWithParams(node, problemCollector); + } + catch (Exception e) { + } + return super.visit(node); + } + }); } - protected void visitAnnotation(SingleMemberAnnotation node, IProblemCollector problemCollector) { - if (!spelExpressionValidationEnabled) { - return; - } - + protected void visitAnnotationWithDefaultParam(SingleMemberAnnotation node, IProblemCollector problemCollector) { 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); - } - } + for (int i = 0; i < reconcilers.length; i++) { + reconcilers[i].visit(node, typeBinding, 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); + protected void visitAnnotationWithParams(NormalAnnotation node, IProblemCollector problemCollector) { + ITypeBinding typeBinding = node.resolveTypeBinding(); + + if (typeBinding != null) { + for (int i = 0; i < reconcilers.length; i++) { + reconcilers[i].visit(node, typeBinding, 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/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/Reconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/Reconciler.java new file mode 100644 index 000000000..40109c3a2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/Reconciler.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.handlers; + +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; + +/** + * @author Martin Lippert + */ +public interface Reconciler { + + void reconcile(String value, int startPosition, IProblemCollector problemCollector); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SpelExpressionReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SpelExpressionReconciler.java new file mode 100644 index 000000000..debf0aa4e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SpelExpressionReconciler.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.handlers; + +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; +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; + +/** + * @author Martin Lippert + */ +public class SpelExpressionReconciler implements Reconciler { + + private boolean spelExpressionValidationEnabled; + + public SpelExpressionReconciler() { + this.spelExpressionValidationEnabled = true; + } + + public void setEnabled(boolean spelExpressionValidationEnabled) { + this.spelExpressionValidationEnabled = spelExpressionValidationEnabled; + } + + @Override + public void reconcile(String spelExpression, int startPosition, IProblemCollector problemCollector) { + if (!this.spelExpressionValidationEnabled) { + return; + } + + if (spelExpression.length() > 0) { + SpelExpressionParser parser = new SpelExpressionParser(); + try { + parser.parseExpression(spelExpression); + } + catch (ParseException e) { + String message = e.getSimpleMessage(); + int position = e.getPosition(); + + createProblem(spelExpression, message, startPosition, position, problemCollector); + } + } + } + + private void createProblem(String spelExpression, String message, int startPosition, int position, IProblemCollector problemCollector) { + int start = startPosition + position; + int length = spelExpression.length() - position; + + ReconcileProblem problem = new ReconcileProblemImpl(ProblemTypes.create("SpEL Expression Problem", ProblemSeverity.ERROR), message, start, length); + problemCollector.accept(problem); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java index bb519bd57..2c07d5666 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java @@ -35,7 +35,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.ide.vscode.boot.app.BootJavaConfig; import org.springframework.ide.vscode.boot.app.BootLanguageServerParams; import org.springframework.ide.vscode.boot.bootiful.AdHocPropertyHarnessTestConf; import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; @@ -177,6 +176,17 @@ public class ValueSpelExpressionValidationTest { assertEquals(0, problems.size()); } + @Test + public void testCorrectSpelExpressionFoundWithParamName() throws Exception { + TextDocument doc = prepareDocument("@Value(\"onField\")", "@Value(value=\"#{new String('hello world').toUpperCase()}\")"); + assertNotNull(doc); + + reconcileEngine.reconcile(doc, problemCollector); + + List problems = problemCollector.getCollectedProblems(); + assertEquals(0, problems.size()); + } + @Test public void testIncorrectSpelExpressionFound() throws Exception { TextDocument doc = prepareDocument("@Value(\"onField\")", "@Value(\"#{new String('hello world).toUpperCase()}\")"); @@ -188,6 +198,83 @@ public class ValueSpelExpressionValidationTest { assertEquals(1, problems.size()); } + @Test + public void testIncorrectSpelExpressionFoundWithParamName() throws Exception { + TextDocument doc = prepareDocument("@Value(\"onField\")", "@Value(value=\"#{new String('hello world).toUpperCase()}\")"); + assertNotNull(doc); + + reconcileEngine.reconcile(doc, problemCollector); + + List problems = problemCollector.getCollectedProblems(); + assertEquals(1, problems.size()); + } + + @Test + public void testIncorrectSpelExpressionFoundOnMethodParameter() throws Exception { + TextDocument doc = prepareDocument("@Value(\"onParameter\")", "@Value(\"#{new String('hello world).toUpperCase()}\")"); + assertNotNull(doc); + + reconcileEngine.reconcile(doc, problemCollector); + + List problems = problemCollector.getCollectedProblems(); + assertEquals(1, problems.size()); + } + + @Test + public void testIncorrectSpelExpressionFoundOnMethodParameterWithParamName() throws Exception { + TextDocument doc = prepareDocument("@Value(\"onParameter\")", "@Value(value=\"#{new String('hello world).toUpperCase()}\")"); + assertNotNull(doc); + + reconcileEngine.reconcile(doc, problemCollector); + + List problems = problemCollector.getCollectedProblems(); + assertEquals(1, problems.size()); + } + + @Test + public void testIncorrectSpelExpressionFoundOnSpelParamOfCachableAnnotation() throws Exception { + TextDocument doc = prepareDocument("@Value(\"onField\")", "@Cacheable(condition=\"#{new String('hello world).toUpperCase()}\")"); + assertNotNull(doc); + + reconcileEngine.reconcile(doc, problemCollector); + + List problems = problemCollector.getCollectedProblems(); + assertEquals(1, problems.size()); + } + + @Test + public void testIncorrectSpelExpressionNotFoundOnNonSpelParamOfCachableAnnotation() throws Exception { + TextDocument doc = prepareDocument("@Value(\"onField\")", "@Cacheable(keyGenerator=\"#{new String('hello world).toUpperCase()}\")"); + assertNotNull(doc); + + reconcileEngine.reconcile(doc, problemCollector); + + List problems = problemCollector.getCollectedProblems(); + assertEquals(0, problems.size()); + } + + @Test + public void testIncorrectSpelExpressionFoundOnSpelParamOfCachableAnnotationAmongOtherParams() throws Exception { + TextDocument doc = prepareDocument("@Value(\"onField\")", "@Cacheable(keyGenerator=\"somekey\", condition=\"#{new String('hello world).toUpperCase()}\")"); + assertNotNull(doc); + + reconcileEngine.reconcile(doc, problemCollector); + + List problems = problemCollector.getCollectedProblems(); + assertEquals(1, problems.size()); + } + + @Test + public void testIncorrectSpelExpressionFoundOnMultipleSpelParamsOfCachableAnnotation() throws Exception { + TextDocument doc = prepareDocument("@Value(\"onField\")", "@Cacheable(unless=\"#{new String('hello world).toUpperCase()}\", condition=\"#{new String('hello world).toUpperCase()}\")"); + assertNotNull(doc); + + reconcileEngine.reconcile(doc, problemCollector); + + List problems = problemCollector.getCollectedProblems(); + assertEquals(2, problems.size()); + } + private TextDocument prepareDocument(String selectedAnnotation, String annotationStatementBeforeTest) throws Exception { String content = IOUtils.toString(new URI(docUri)); diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/TestValueCompletion.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/TestValueCompletion.java index ba853018a..9b5507635 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/TestValueCompletion.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotations/src/main/java/org/test/TestValueCompletion.java @@ -1,6 +1,7 @@ package org.test; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; public class TestValueCompletion {