refactored spel syntax validation and enabled it for additional annotations and named parameters

This commit is contained in:
Martin Lippert
2020-07-11 22:09:04 +02:00
parent 7c19adfa35
commit 11df688a3d
6 changed files with 330 additions and 50 deletions

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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<ReconcileProblem> 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<ReconcileProblem> 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<ReconcileProblem> 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<ReconcileProblem> 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<ReconcileProblem> 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<ReconcileProblem> 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<ReconcileProblem> 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<ReconcileProblem> problems = problemCollector.getCollectedProblems();
assertEquals(2, problems.size());
}
private TextDocument prepareDocument(String selectedAnnotation, String annotationStatementBeforeTest) throws Exception {
String content = IOUtils.toString(new URI(docUri));

View File

@@ -1,6 +1,7 @@
package org.test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
public class TestValueCompletion {