1
+ /*******************************************************************************
2
+ * Copyright (c) 2017, 2024 Broadcom, Inc.
3
+ * All rights reserved. This program and the accompanying materials
4
+ * are made available under the terms of the Eclipse Public License v1.0
5
+ * which accompanies this distribution, and is available at
6
+ * https://www.eclipse.org/legal/epl-v10.html
7
+ *
8
+ * Contributors:
9
+ * Broadcom, Inc. - initial API and implementation
10
+ *******************************************************************************/
11
+ package org .springframework .ide .vscode .boot .java .contextconfiguration ;
12
+
13
+ import static org .springframework .ide .vscode .commons .util .StringUtil .camelCaseToHyphens ;
14
+
15
+ import java .nio .file .Paths ;
16
+ import java .util .*;
17
+ import java .util .stream .Collectors ;
18
+
19
+ import org .eclipse .jdt .core .dom .ASTNode ;
20
+ import org .eclipse .jdt .core .dom .Annotation ;
21
+ import org .eclipse .jdt .core .dom .ITypeBinding ;
22
+ import org .eclipse .jdt .core .dom .MemberValuePair ;
23
+ import org .eclipse .jdt .core .dom .QualifiedName ;
24
+ import org .eclipse .jdt .core .dom .SimpleName ;
25
+ import org .eclipse .jdt .core .dom .StringLiteral ;
26
+ import org .eclipse .lsp4j .TextDocumentIdentifier ;
27
+ import org .openrewrite .yaml .internal .grammar .JsonPathParser ;
28
+ import org .slf4j .Logger ;
29
+ import org .slf4j .LoggerFactory ;
30
+ import org .springframework .ide .vscode .boot .java .annotations .AnnotationAttributeCompletionProposal ;
31
+ import org .springframework .ide .vscode .boot .java .handlers .CompletionProvider ;
32
+ import org .springframework .ide .vscode .boot .metadata .ProjectBasedPropertyIndexProvider ;
33
+ import org .springframework .ide .vscode .boot .metadata .PropertyInfo ;
34
+ import org .springframework .ide .vscode .boot .metadata .SpringPropertyIndexProvider ;
35
+ import org .springframework .ide .vscode .commons .java .IClasspathUtil ;
36
+ import org .springframework .ide .vscode .commons .java .IJavaProject ;
37
+ import org .springframework .ide .vscode .commons .languageserver .completion .DocumentEdits ;
38
+ import org .springframework .ide .vscode .commons .languageserver .completion .ICompletionProposal ;
39
+ import org .springframework .ide .vscode .commons .languageserver .java .JavaProjectFinder ;
40
+ import org .springframework .ide .vscode .commons .util .BadLocationException ;
41
+ import org .springframework .ide .vscode .commons .util .FuzzyMap ;
42
+ import org .springframework .ide .vscode .commons .util .FuzzyMap .Match ;
43
+ import org .springframework .ide .vscode .commons .util .text .IDocument ;
44
+ import org .springframework .ide .vscode .commons .util .text .TextDocument ;
45
+
46
+ /**
47
+ * @author Karthik Sankaranarayanan
48
+ */
49
+ public class ContextConfigurationProcessor implements CompletionProvider {
50
+
51
+ private static final Logger log = LoggerFactory .getLogger (ContextConfigurationProcessor .class );
52
+
53
+ private final JavaProjectFinder projectFinder ;
54
+
55
+ public ContextConfigurationProcessor (JavaProjectFinder projectFinder ) {
56
+ this .projectFinder = projectFinder ;
57
+ }
58
+
59
+ @ Override
60
+ public void provideCompletions (ASTNode node , Annotation annotation , ITypeBinding type ,
61
+ int offset , TextDocument doc , Collection <ICompletionProposal > completions ) {
62
+
63
+ try {
64
+ Optional <IJavaProject > optionalProject = this .projectFinder .find (doc .getId ());
65
+ if (optionalProject .isEmpty ()) {
66
+ return ;
67
+ }
68
+
69
+ IJavaProject project = optionalProject .get ();
70
+
71
+ // case: @ContextConfiguration(<*>)
72
+ if (node == annotation && doc .get (offset - 1 , 2 ).endsWith ("()" )) {
73
+ addClasspathResourceProposals (project , doc , offset , offset , "" , true , completions );
74
+ }
75
+ // case: @ContextConfiguration(prefix<*>)
76
+ else if (node instanceof SimpleName && node .getParent () instanceof Annotation ) {
77
+ computeProposalsForSimpleName (project , node , completions , offset , doc );
78
+ }
79
+ // case: @ContextConfiguration(file.ext<*>) - the "." causes a QualifierNode to be generated
80
+ else if (node instanceof SimpleName && node .getParent () instanceof QualifiedName && node .getParent ().getParent () instanceof Annotation ) {
81
+ computeProposalsForSimpleName (project , node .getParent (), completions , offset , doc );
82
+ }
83
+ // case: @ContextConfiguration(locations=<*>) || @ContextConfiguration(value=<*>)
84
+ else if (node instanceof SimpleName && node .getParent () instanceof MemberValuePair
85
+ && ("locations" .equals (((MemberValuePair )node .getParent ()).getName ().toString ()) || "value" .equals (((MemberValuePair )node .getParent ()).getName ().toString ()))) {
86
+ computeProposalsForSimpleName (project , node , completions , offset , doc );
87
+ }
88
+ // case: @ContextConfiguration(locations=<*>) || @ContextConfiguration(value=<*>)
89
+ else if (node instanceof SimpleName && node .getParent () instanceof QualifiedName && node .getParent ().getParent () instanceof MemberValuePair
90
+ && ("locations" .equals (((MemberValuePair )node .getParent ().getParent ()).getName ().toString ()) || "value" .equals (((MemberValuePair )node .getParent ().getParent ()).getName ().toString ()))) {
91
+ computeProposalsForSimpleName (project , node .getParent (), completions , offset , doc );
92
+ }
93
+ // case: @ContextConfiguration("prefix<*>")
94
+ else if (node instanceof StringLiteral && node .getParent () instanceof Annotation ) {
95
+ if (node .toString ().startsWith ("\" " ) && node .toString ().endsWith ("\" " )) {
96
+ computeProposalsForStringLiteral (project , (StringLiteral ) node , completions , offset , doc );
97
+ }
98
+ }
99
+ // case:@ContextConfiguration(locations="prefix<*>") || @ContextConfiguration(value="prefix<*>")
100
+ else if (node instanceof StringLiteral && node .getParent () instanceof MemberValuePair
101
+ && ("locations" .equals (((MemberValuePair )node .getParent ()).getName ().toString ()) || "value" .equals (((MemberValuePair )node .getParent ()).getName ().toString ()))) {
102
+ if (node .toString ().startsWith ("\" " ) && node .toString ().endsWith ("\" " )) {
103
+ computeProposalsForStringLiteral (project , (StringLiteral ) node , completions , offset , doc );
104
+ }
105
+ }
106
+
107
+ }
108
+ catch (Exception e ) {
109
+ log .error ("problem while looking for ContextConfiguration annotation proposals" , e );
110
+ }
111
+ }
112
+
113
+ private void addClasspathResourceProposals (IJavaProject project , TextDocument doc , int startOffset , int endOffset , String prefix , boolean includeQuotes , Collection <ICompletionProposal > completions ) {
114
+ String [] resources = findResources (project , prefix );
115
+ List <String > result = Arrays .asList (resources );
116
+ List <String > filteredResult = result .stream ().filter (f -> f .endsWith (".xml" )).toList ();
117
+ double score = resources .length + 1000 ;
118
+ for (String resource : filteredResult ) {
119
+
120
+ DocumentEdits edits = new DocumentEdits (doc , false );
121
+
122
+ if (includeQuotes ) {
123
+ edits .replace (startOffset , endOffset , "\" " + "/" + resource + "\" " );
124
+ }
125
+ else {
126
+ edits .replace (startOffset , endOffset , "/" + resource );
127
+ }
128
+
129
+ String label = "/" + resource ;
130
+
131
+ ICompletionProposal proposal = new AnnotationAttributeCompletionProposal (edits , label , label , null , score --);
132
+ completions .add (proposal );
133
+ }
134
+
135
+ }
136
+
137
+ private void computeProposalsForSimpleName (IJavaProject project , ASTNode node , Collection <ICompletionProposal > completions , int offset , TextDocument doc ) {
138
+ int startOffset = node .getStartPosition ();
139
+ int endOffset = node .getStartPosition () + node .getLength ();
140
+
141
+ String unfilteredPrefix = node .toString ().substring (0 , offset - node .getStartPosition ());
142
+ addClasspathResourceProposals (project , doc , startOffset , endOffset , unfilteredPrefix , true , completions );
143
+ }
144
+
145
+ private void computeProposalsForStringLiteral (IJavaProject project , StringLiteral node , Collection <ICompletionProposal > completions , int offset , TextDocument doc ) throws BadLocationException {
146
+ String prefix = identifyPropertyPrefix (doc .get (node .getStartPosition () + 1 , offset - (node .getStartPosition () + 1 )), offset - (node .getStartPosition () + 1 ));
147
+
148
+ int startOffset = offset - prefix .length ();
149
+ int endOffset = offset ;
150
+
151
+ String unfilteredPrefix = node .getLiteralValue ().substring (0 , offset - (node .getStartPosition () + 1 ));
152
+ addClasspathResourceProposals (project , doc , startOffset , endOffset , unfilteredPrefix , false , completions );
153
+ }
154
+
155
+ public String identifyPropertyPrefix (String nodeContent , int offset ) {
156
+ String result = nodeContent .substring (0 , offset );
157
+
158
+ int i = offset - 1 ;
159
+ while (i >= 0 ) {
160
+ char c = nodeContent .charAt (i );
161
+ if (c == '}' || c == '{' || c == '$' || c == '#' ) {
162
+ result = result .substring (i + 1 , offset );
163
+ break ;
164
+ }
165
+ i --;
166
+ }
167
+
168
+ return result ;
169
+ }
170
+
171
+ private String [] findResources (IJavaProject project , String prefix ) {
172
+ String [] resources = IClasspathUtil .getClasspathResources (project .getClasspath ()).stream ()
173
+ .distinct ()
174
+ .sorted (new Comparator <String >() {
175
+ @ Override
176
+ public int compare (String o1 , String o2 ) {
177
+ return Paths .get (o1 ).compareTo (Paths .get (o2 ));
178
+ }
179
+ })
180
+ .map (r -> r .replaceAll ("\\ \\ " , "/" ))
181
+ .filter (r -> ("classpath:" + r ).contains (prefix ))
182
+ .toArray (String []::new );
183
+
184
+ return resources ;
185
+ }
186
+
187
+ }
0 commit comments