Skip to content

Commit b26d6da

Browse files
committed
Allow SPEL or Property Holder in the cron parameter of @Scheduled
1 parent ae76991 commit b26d6da

File tree

7 files changed

+164
-7
lines changed

7 files changed

+164
-7
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconciler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public boolean visit(NormalAnnotation node) {
5858
if (value instanceof MemberValuePair) {
5959
MemberValuePair pair = (MemberValuePair) value;
6060
String name = pair.getName().getFullyQualifiedName();
61-
if (name != null && "cron".equals(name)) {
61+
if (name != null && "cron".equals(name) && JdtCronSemanticTokensProvider.isCronExpression(pair.getValue())) {
6262
QueryJdtAstReconciler.reconcileExpression(cronReconciler, pair.getValue(), problemCollector);
6363
}
6464
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProvider.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
import org.eclipse.jdt.core.dom.ASTVisitor;
1616
import org.eclipse.jdt.core.dom.CompilationUnit;
17+
import org.eclipse.jdt.core.dom.Expression;
1718
import org.eclipse.jdt.core.dom.ITypeBinding;
1819
import org.eclipse.jdt.core.dom.MemberValuePair;
1920
import org.eclipse.jdt.core.dom.NormalAnnotation;
21+
import org.eclipse.jdt.core.dom.StringLiteral;
22+
import org.eclipse.jdt.core.dom.TextBlock;
2023
import org.springframework.ide.vscode.boot.java.Annotations;
2124
import org.springframework.ide.vscode.boot.java.JdtSemanticTokensProvider;
2225
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider;
@@ -68,7 +71,7 @@ public boolean visit(NormalAnnotation node) {
6871
if (value instanceof MemberValuePair) {
6972
MemberValuePair pair = (MemberValuePair) value;
7073
String name = pair.getName().getFullyQualifiedName();
71-
if (name != null && "cron".equals(name)) {
74+
if (name != null && "cron".equals(name) && isCronExpression(pair.getValue())) {
7275
JdtDataQuerySemanticTokensProvider.computeTokensForExpression(tokensProvider, pair.getValue()).forEach(collector::accept);
7376
}
7477
}
@@ -81,5 +84,20 @@ public boolean visit(NormalAnnotation node) {
8184

8285
};
8386
}
87+
88+
public static boolean isCronExpression(Expression e) {
89+
String value = null;
90+
if (e instanceof StringLiteral sl) {
91+
value = sl.getLiteralValue();
92+
} else if (e instanceof TextBlock tb) {
93+
value = tb.getLiteralValue();
94+
}
95+
value = value.trim();
96+
if (value.startsWith("#{") || value.startsWith("${")) {
97+
// Either SPEL or Property Holder
98+
return false;
99+
}
100+
return value != null;
101+
}
84102

85103
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java

+13-5
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ public final class AnnotationParamSpelExtractor {
5858
new AnnotationParamSpelExtractor(SPRING_POST_FILTER, "value", "", ""),
5959

6060
new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, null, "", ""),
61-
new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "", "")
61+
new AnnotationParamSpelExtractor(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "", ""),
62+
63+
new AnnotationParamSpelExtractor(Annotations.SCHEDULED, "cron", "#{", "}"),
6264
};
6365

6466

@@ -125,10 +127,16 @@ public Optional<Snippet> getSpelRegion(SingleMemberAnnotation a) {
125127
private Optional<Snippet> fromStringLiteral(StringLiteral valueExp) {
126128
String value = valueExp.getEscapedValue();
127129
value = value.substring(1, value.length() - 1);
128-
if (value != null && value.startsWith(paramValuePrefix) && value.endsWith(paramValuePostfix)) {
129-
String spelText = value.substring(paramValuePrefix.length(), value.length() - paramValuePostfix.length());
130-
int offset = valueExp.getStartPosition() + paramValuePrefix.length() + 1;
131-
return Optional.of(new Snippet(spelText, offset));
130+
if (value != null) {
131+
int startIdx = value.indexOf(paramValuePrefix);
132+
if (startIdx >= 0) {
133+
int endIdx = value.lastIndexOf(paramValuePostfix);
134+
if (endIdx >= 0) {
135+
String spelText = value.substring(startIdx + paramValuePrefix.length(), endIdx);
136+
int offset = valueExp.getStartPosition() + startIdx + paramValuePrefix.length() + 1;
137+
return Optional.of(new Snippet(spelText, offset));
138+
}
139+
}
132140
}
133141
return Optional.empty();
134142
}

headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/CronSemanticTokensTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,5 @@ void errors_2() {
274274
assertThat(tokens.get(11)).isEqualTo(new SemanticTokenData(24, 25, "operator", new String[0])); // -
275275
assertThat(tokens.get(12)).isEqualTo(new SemanticTokenData(25, 30, "enum", new String[0])); // MARCH
276276
}
277+
277278
}

headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconcilerTest.java

+40
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,46 @@ void foo() {}
8787
editor.assertProblems();
8888
}
8989

90+
@Test
91+
void noErrors_PropertyHolder() throws Exception {
92+
String source = """
93+
package example.demo;
94+
95+
import org.springframework.scheduling.annotation.Scheduled;
96+
97+
public class A {
98+
99+
@Scheduled(cron = " ${demo.cron} ")
100+
void foo() {}
101+
102+
}
103+
""";
104+
String docUri = directory.toPath().resolve("src/main/java/example/demo/A.java").toUri()
105+
.toString();
106+
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
107+
editor.assertProblems();
108+
}
109+
110+
@Test
111+
void noErrors_SPEL() throws Exception {
112+
String source = """
113+
package example.demo;
114+
115+
import org.springframework.scheduling.annotation.Scheduled;
116+
117+
public class A {
118+
119+
@Scheduled(cron = " #{demo.cron} ")
120+
void foo() {}
121+
122+
}
123+
""";
124+
String docUri = directory.toPath().resolve("src/main/java/example/demo/A.java").toUri()
125+
.toString();
126+
Editor editor = harness.newEditor(LanguageId.JAVA, source, docUri);
127+
editor.assertProblems();
128+
}
129+
90130
@Test
91131
void errorsReported_1() throws Exception {
92132
String source = """

headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProviderTest.java

+52
Original file line numberDiff line numberDiff line change
@@ -367,4 +367,56 @@ void foo() {}
367367
assertThat(token).isEqualTo(new SemanticTokenData(142, 147, "enum", new String[0]));
368368
assertThat(source.substring(token.start(), token.end())).isEqualTo("MARCH");
369369
}
370+
371+
@Test
372+
void noTokens_SPEL() throws Exception {
373+
String source = """
374+
package my.package
375+
376+
import org.springframework.scheduling.annotation.Scheduled;
377+
378+
public class A {
379+
380+
@Scheduled(cron=" #{demo.cron} ")
381+
void foo() {}
382+
383+
}
384+
""";
385+
386+
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/A.java").toUri()
387+
.toASCIIString();
388+
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "A.java", jp);
389+
390+
assertThat(cu).isNotNull();
391+
392+
List<SemanticTokenData> tokens = computeTokens(cu);
393+
394+
assertThat(tokens.size()).isEqualTo(0);
395+
}
396+
397+
@Test
398+
void noTokens_PropertyHolder() throws Exception {
399+
String source = """
400+
package my.package
401+
402+
import org.springframework.scheduling.annotation.Scheduled;
403+
404+
public class A {
405+
406+
@Scheduled(cron=" ${demo.cron} ")
407+
void foo() {}
408+
409+
}
410+
""";
411+
412+
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/A.java").toUri()
413+
.toASCIIString();
414+
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "A.java", jp);
415+
416+
assertThat(cu).isNotNull();
417+
418+
List<SemanticTokenData> tokens = computeTokens(cu);
419+
420+
assertThat(tokens.size()).isEqualTo(0);
421+
}
370422
}

headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/spel/JdtSpelSemanticTokensProviderTest.java

+38
Original file line numberDiff line numberDiff line change
@@ -154,5 +154,43 @@ public class Owner {
154154
assertThat(source.substring(token.start(), token.end())).isEqualTo("'[a-zA-Z\\s]+'");
155155

156156
}
157+
158+
@Test
159+
void leadingAndTrailingSpaces() throws Exception {
160+
String source = """
161+
package my.package
162+
163+
import org.springframework.scheduling.annotation.Scheduled;
164+
165+
public class A {
166+
167+
@Scheduled(cron=" #{demo.cron} ")
168+
void foo() {}
169+
170+
}
171+
""";
172+
173+
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/A.java").toUri()
174+
.toASCIIString();
175+
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "A.java", jp);
176+
177+
assertThat(cu).isNotNull();
178+
179+
List<SemanticTokenData> tokens = computeTokens(cu);
180+
181+
assertThat(tokens.size()).isEqualTo(3);
182+
183+
SemanticTokenData token = tokens.get(0);
184+
assertThat(token).isEqualTo(new SemanticTokenData(121, 125, "variable", new String[0]));
185+
assertThat(source.substring(token.start(), token.end())).isEqualTo("demo");
186+
187+
token = tokens.get(1);
188+
assertThat(token).isEqualTo(new SemanticTokenData(125, 126, "operator", new String[0]));
189+
assertThat(source.substring(token.start(), token.end())).isEqualTo(".");
190+
191+
token = tokens.get(2);
192+
assertThat(token).isEqualTo(new SemanticTokenData(126, 130, "property", new String[0]));
193+
assertThat(source.substring(token.start(), token.end())).isEqualTo("cron");
194+
}
157195

158196
}

0 commit comments

Comments
 (0)