Skip to content

Commit 374a6ef

Browse files
committed
Spring Data JPA Content Assist
Fixes gh-XXXX
1 parent 6979975 commit 374a6ef

File tree

8 files changed

+552
-56
lines changed

8 files changed

+552
-56
lines changed

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public void provideCompletions(ASTNode node, int offset, IDocument doc, Collecti
5757
for (DomainProperty property : properties) {
5858
completions.add(generateCompletionProposal(offset, prefix, repo, property));
5959
}
60+
DataRepositoryPrefixSensitiveCompletionProvider.addPrefixSensitiveProposals(completions, offset, prefix, repo);
6061
}
6162
}
6263
}
@@ -71,7 +72,6 @@ protected ICompletionProposal generateCompletionProposal(int offset, String pref
7172
label.append(StringUtils.uncapitalize(domainProperty.getName()));
7273
label.append(");");
7374

74-
DocumentEdits edits = new DocumentEdits(null, false);
7575

7676
StringBuilder completion = new StringBuilder();
7777
completion.append("List<");
@@ -84,20 +84,25 @@ protected ICompletionProposal generateCompletionProposal(int offset, String pref
8484
completion.append(StringUtils.uncapitalize(domainProperty.getName()));
8585
completion.append(");");
8686

87-
String filter = label.toString();
88-
if (prefix != null && label.toString().startsWith(prefix)) {
89-
edits.replace(offset - prefix.length(), offset, completion.toString());
87+
return createProposal(offset, CompletionItemKind.Method, prefix, label.toString(), completion.toString());
88+
}
89+
90+
static ICompletionProposal createProposal(int offset, CompletionItemKind completionItemKind, String prefix, String label, String completion) {
91+
DocumentEdits edits = new DocumentEdits(null, false);
92+
String filter = label;
93+
if (prefix != null && label.startsWith(prefix)) {
94+
edits.replace(offset - prefix.length(), offset, completion);
9095
}
91-
else if (prefix != null && completion.toString().startsWith(prefix)) {
92-
edits.replace(offset - prefix.length(), offset, completion.toString());
93-
filter = completion.toString();
96+
else if (prefix != null && completion.startsWith(prefix)) {
97+
edits.replace(offset - prefix.length(), offset, completion);
98+
filter = completion;
9499
}
95100
else {
96-
edits.insert(offset, completion.toString());
101+
edits.insert(offset, completion);
97102
}
98103

99104
DocumentEdits additionalEdits = new DocumentEdits(null, false);
100-
return new FindByCompletionProposal(label.toString(), CompletionItemKind.Method, edits, null, null, Optional.of(additionalEdits), filter);
105+
return new FindByCompletionProposal(label, completionItemKind, edits, null, null, Optional.of(additionalEdits), filter);
101106
}
102107

103108
private DataRepositoryDefinition getDataRepositoryDefinition(TypeDeclaration type) {

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

Lines changed: 357 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ public DomainType(String packageName, String fullName, String simpleName) {
3636
}
3737

3838
public DomainType(ITypeBinding typeBinding) {
39-
this.packageName = typeBinding.getPackage().getName();
39+
if (typeBinding.getPackage() == null) {
40+
this.packageName = "";
41+
} else {
42+
this.packageName = typeBinding.getPackage().getName();
43+
}
4044
this.fullName = typeBinding.getQualifiedName();
4145
this.simpleName = typeBinding.getName();
4246

@@ -48,9 +52,17 @@ public DomainType(ITypeBinding typeBinding) {
4852

4953
for (IMethodBinding method : methods) {
5054
String methodName = method.getName();
51-
if (methodName != null && methodName.startsWith("get")) {
52-
String propertyName = methodName.substring(3);
53-
properties.add(new DomainProperty(propertyName, new DomainType(method.getReturnType())));
55+
if (methodName != null) {
56+
String propertyName = null;
57+
if (methodName.startsWith("get")) {
58+
propertyName = methodName.substring(3);
59+
}
60+
else if (methodName.startsWith("is")) {
61+
propertyName = methodName.substring(2);
62+
}
63+
if (propertyName != null) {
64+
properties.add(new DomainProperty(propertyName, new DomainType(method.getReturnType())));
65+
}
5466
}
5567
}
5668
return (DomainProperty[]) properties.toArray(new DomainProperty[properties.size()]);

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

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2018 Pivotal, Inc.
2+
* Copyright (c) 2018, 2023 Pivotal, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -11,6 +11,7 @@
1111
package org.springframework.ide.vscode.boot.java.data.test;
1212

1313
import java.io.InputStream;
14+
import java.util.Arrays;
1415
import java.util.List;
1516

1617
import org.apache.commons.io.IOUtils;
@@ -27,13 +28,13 @@
2728
import org.springframework.ide.vscode.commons.java.IJavaProject;
2829
import org.springframework.ide.vscode.commons.util.text.LanguageId;
2930
import org.springframework.ide.vscode.languageserver.testharness.Editor;
30-
import org.springframework.ide.vscode.languageserver.testharness.TestAsserts;
3131
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
3232
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
3333
import org.springframework.test.context.junit.jupiter.SpringExtension;
3434

3535
/**
3636
* @author Martin Lippert
37+
* @author danthe1st
3738
*/
3839
@ExtendWith(SpringExtension.class)
3940
@BootLanguageServerTest
@@ -53,11 +54,78 @@ public void setup() throws Exception {
5354
@Test
5455
void testStandardFindByCompletions() throws Exception {
5556
prepareCase("{", "{<*>");
56-
assertContainsAnnotationCompletions(
57+
assertStandardCompletions();
58+
}
59+
60+
@Test
61+
void testPrefixSensitiveCompletionsNoPrefix() throws Exception {
62+
prepareCase("{\n}", "{\n<*>");
63+
assertStandardCompletions();
64+
}
65+
66+
private void assertStandardCompletions() throws Exception {
67+
assertContainsAnnotationCompletions(
68+
"countBy",
69+
"deleteBy",
70+
"existsBy",
71+
"findBy",
5772
"List<Customer> findByFirstName(String firstName);",
58-
"List<Customer> findByLastName(String lastName);");
73+
"List<Customer> findById(Long id);",
74+
"List<Customer> findByLastName(String lastName);",
75+
"List<Customer> findByResponsibleEmployee(Employee responsibleEmployee);",
76+
"getBy",
77+
"queryBy",
78+
"readBy",
79+
"removeBy",
80+
"searchBy",
81+
"streamBy");
82+
}
83+
84+
@Test
85+
void testPrefixSensitiveCompletionsCompleteMethod() throws Exception {
86+
checkCompletions("findByFirstNameAndLastName", "List<Customer> findByFirstNameAndLastName(String firstName, String lastName);");
5987
}
6088

89+
@Test
90+
void testAttributeComparison() throws Exception {
91+
checkCompletions("findByFirstNameIsGreaterThanLastName", "List<Customer> findByFirstNameIsGreaterThanLastName();");
92+
}
93+
94+
@Test
95+
void testTerminatingKeyword() throws Exception {
96+
checkCompletions("findByFirstNameNull", "List<Customer> findByFirstNameNull();");
97+
checkCompletions("findByFirstNameNotNull", "List<Customer> findByFirstNameNotNull();");
98+
}
99+
100+
@Test
101+
void testNewConditionAfterTerminatedExpression() throws Exception {
102+
checkCompletions("findByFirstNameNullAndLastName", "List<Customer> findByFirstNameNullAndLastName(String lastName);");
103+
checkCompletions("findByNotFirstNameNullAndNotLastName", "List<Customer> findByNotFirstNameNullAndNotLastName(String lastName);");
104+
}
105+
106+
@Test
107+
void testDifferentSubjectTypes() throws Exception {
108+
checkCompletions("existsByFirstName", "boolean existsByFirstName(String firstName);");
109+
checkCompletions("countByFirstName", "long countByFirstName(String firstName);");
110+
checkCompletions("streamByFirstName", "Streamable<Customer> streamByFirstName(String firstName);");
111+
checkCompletions("removeByFirstName", "void removeByFirstName(String firstName);");
112+
}
113+
114+
@Test
115+
void testUnknownAttribute() throws Exception {
116+
checkCompletions("findByUnknownObject", "List<Customer> findByUnknownObject(Object unknownObject);");
117+
}
118+
119+
@Test
120+
void testKeywordInPredicate() throws Exception {
121+
checkCompletions("findByThisCustomerIsSpecial", "List<Customer> findByThisCustomerIsSpecial(boolean thisCustomerIsSpecial);");
122+
}
123+
124+
private void checkCompletions(String alredyPresent, String... expectedCompletions) throws Exception {
125+
prepareCase("{\n}", "{\n\t" + alredyPresent + "<*>");
126+
assertContainsAnnotationCompletions(Arrays.stream(expectedCompletions).map(expected -> expected + "<*>").toArray(String[]::new));
127+
}
128+
61129
private void prepareCase(String selectedAnnotation, String annotationStatementBeforeTest) throws Exception {
62130
InputStream resource = this.getClass().getResourceAsStream("/test-projects/test-spring-data-symbols/src/main/java/org/test/TestCustomerRepositoryForCompletions.java");
63131
String content = IOUtils.toString(resource);
@@ -74,13 +142,10 @@ private void assertContainsAnnotationCompletions(String... expectedResultsFromCo
74142
Editor clonedEditor = editor.clone();
75143
clonedEditor.apply(foundCompletion);
76144

77-
if (clonedEditor.getText().contains(expectedResultsFromCompletion[i])) {
145+
if (i < expectedResultsFromCompletion.length && clonedEditor.getText().contains(expectedResultsFromCompletion[i])) {
78146
i++;
79147
}
80148
}
81-
82149
assertEquals(expectedResultsFromCompletion.length, i);
83150
}
84-
85-
86151
}

headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Application.java

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,54 @@
22

33
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
5-
65
import org.springframework.boot.CommandLineRunner;
76
import org.springframework.boot.SpringApplication;
87
import org.springframework.boot.autoconfigure.SpringBootApplication;
98
import org.springframework.context.annotation.Bean;
109

1110
@SpringBootApplication
1211
public class Application {
13-
12+
1413
private static final Logger log = LoggerFactory.getLogger(Application.class);
15-
14+
1615
public static void main(String[] args) {
1716
SpringApplication.run(Application.class);
1817
}
19-
18+
2019
@Bean
2120
public CommandLineRunner demo(CustomerRepository repository) {
22-
return (args) -> {
21+
return args -> {
22+
Employee employee = new Employee("Margot", "Al-Harazi");
2323
// save a couple of customers
24-
repository.save(new Customer("Jack", "Bauer"));
25-
repository.save(new Customer("Chloe", "O'Brian"));
26-
repository.save(new Customer("Kim", "Bauer"));
27-
repository.save(new Customer("David", "Palmer"));
28-
repository.save(new Customer("Michelle", "Dessler"));
29-
24+
repository.save(new Customer("Jack", "Bauer", employee));
25+
repository.save(new Customer("Chloe", "O'Brian", employee));
26+
repository.save(new Customer("Kim", "Bauer", employee));
27+
repository.save(new Customer("David", "Palmer", employee));
28+
repository.save(new Customer("Michelle", "Dessler", employee));
29+
3030
// fetch all customers
3131
log.info("Customers found with findAll():");
3232
log.info("-------------------------------");
33-
for (Customer customer : repository.findAll()) {
33+
for(Customer customer : repository.findAll()){
3434
log.info(customer.toString());
3535
}
3636
log.info("");
37-
37+
3838
// fetch an individual customer by ID
3939
Customer customer = repository.findOne(1L);
4040
log.info("Customer found with findOne(1L):");
4141
log.info("--------------------------------");
4242
log.info(customer.toString());
4343
log.info("");
44-
44+
4545
// fetch customers by last name
4646
log.info("Customer found with findByLastName('Bauer'):");
4747
log.info("--------------------------------------------");
48-
for (Customer bauer : repository.findByLastName("Bauer")) {
48+
for(Customer bauer : repository.findByLastName("Bauer")){
4949
log.info(bauer.toString());
5050
}
5151
log.info("");
5252
};
5353
}
54-
54+
5555
}

headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Customer.java

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,34 @@
55
import javax.persistence.GeneratedValue;
66
import javax.persistence.GenerationType;
77
import javax.persistence.Id;
8+
import javax.persistence.ManyToOne;
89

910
@Entity
1011
public class Customer {
1112

12-
@Id
13-
@GeneratedValue(strategy=GenerationType.AUTO)
14-
private Long id;
15-
private String firstName;
16-
private String lastName;
13+
@Id
14+
@GeneratedValue(strategy = GenerationType.AUTO)
15+
private Long id;
16+
private String firstName;
17+
private String lastName;
18+
private boolean thisCustomerIsSpecial;//contains keyword in name
1719

18-
protected Customer() {}
20+
@ManyToOne
21+
private Employee responsibleEmployee;
1922

20-
public Customer(String firstName, String lastName) {
21-
this.firstName = firstName;
22-
this.lastName = lastName;
23-
}
23+
protected Customer() {}
2424

25-
@Override
26-
public String toString() {
27-
return String.format(
28-
"Customer[id=%d, firstName='%s', lastName='%s']",
29-
id, firstName, lastName);
30-
}
25+
public Customer(String firstName, String lastName, Employee responsibleEmployee) {
26+
this.firstName = firstName;
27+
this.lastName = lastName;
28+
this.responsibleEmployee = responsibleEmployee;
29+
}
30+
31+
@Override
32+
public String toString() {
33+
return "Customer [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", responsibleEmployee="
34+
+ responsibleEmployee + "]";
35+
}
3136

3237
// end::sample[]
3338

@@ -42,5 +47,12 @@ public String getFirstName() {
4247
public String getLastName() {
4348
return lastName;
4449
}
45-
}
4650

51+
public boolean isThisCustomerIsSpecial() {
52+
return thisCustomerIsSpecial;
53+
}
54+
55+
public Employee getResponsibleEmployee() {
56+
return responsibleEmployee;
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// tag::sample[]
2+
package org.test;
3+
4+
import javax.persistence.Entity;
5+
import javax.persistence.GeneratedValue;
6+
import javax.persistence.GenerationType;
7+
import javax.persistence.Id;
8+
9+
@Entity
10+
public class Employee {
11+
12+
@Id
13+
@GeneratedValue(strategy = GenerationType.AUTO)
14+
private Long id;
15+
private String firstName;
16+
private String lastName;
17+
18+
protected Employee() {
19+
}
20+
21+
public Employee(String firstName, String lastName) {
22+
this.firstName = firstName;
23+
this.lastName = lastName;
24+
}
25+
26+
@Override
27+
public String toString() {
28+
return String.format(
29+
"Employee[id=%d, firstName='%s', lastName='%s']",
30+
id, firstName, lastName
31+
);
32+
}
33+
34+
// end::sample[]
35+
36+
public Long getId() {
37+
return id;
38+
}
39+
40+
public String getFirstName() {
41+
return firstName;
42+
}
43+
44+
public String getLastName() {
45+
return lastName;
46+
}
47+
}

0 commit comments

Comments
 (0)