Spring Data JPA Content Assist

This commit is contained in:
danthe1st
2023-02-20 15:20:15 +01:00
committed by aboyko
parent 4584be1fe8
commit 97d66062c0
13 changed files with 812 additions and 50 deletions

View File

@@ -57,10 +57,13 @@ public class DataRepositoryCompletionProcessor implements CompletionProvider {
for (DomainProperty property : properties) {
completions.add(generateCompletionProposal(offset, prefix, repo, property));
}
DataRepositoryPrefixSensitiveCompletionProvider.addPrefixSensitiveProposals(completions, doc, offset, prefix, repo);
}
}
}
protected ICompletionProposal generateCompletionProposal(int offset, String prefix, DataRepositoryDefinition repoDef, DomainProperty domainProperty) {
StringBuilder label = new StringBuilder();
label.append("findBy");
@@ -71,7 +74,6 @@ public class DataRepositoryCompletionProcessor implements CompletionProvider {
label.append(StringUtils.uncapitalize(domainProperty.getName()));
label.append(");");
DocumentEdits edits = new DocumentEdits(null, false);
StringBuilder completion = new StringBuilder();
completion.append("List<");
@@ -84,20 +86,25 @@ public class DataRepositoryCompletionProcessor implements CompletionProvider {
completion.append(StringUtils.uncapitalize(domainProperty.getName()));
completion.append(");");
String filter = label.toString();
if (prefix != null && label.toString().startsWith(prefix)) {
edits.replace(offset - prefix.length(), offset, completion.toString());
return createProposal(offset, CompletionItemKind.Method, prefix, label.toString(), completion.toString());
}
static ICompletionProposal createProposal(int offset, CompletionItemKind completionItemKind, String prefix, String label, String completion) {
DocumentEdits edits = new DocumentEdits(null, false);
String filter = label;
if (prefix != null && label.startsWith(prefix)) {
edits.replace(offset - prefix.length(), offset, completion);
}
else if (prefix != null && completion.toString().startsWith(prefix)) {
edits.replace(offset - prefix.length(), offset, completion.toString());
filter = completion.toString();
else if (prefix != null && completion.startsWith(prefix)) {
edits.replace(offset - prefix.length(), offset, completion);
filter = completion;
}
else {
edits.insert(offset, completion.toString());
edits.insert(offset, completion);
}
DocumentEdits additionalEdits = new DocumentEdits(null, false);
return new FindByCompletionProposal(label.toString(), CompletionItemKind.Method, edits, null, null, Optional.of(additionalEdits), filter);
return new FindByCompletionProposal(label, completionItemKind, edits, null, null, Optional.of(additionalEdits), filter);
}
private DataRepositoryDefinition getDataRepositoryDefinition(TypeDeclaration type) {

View File

@@ -0,0 +1,42 @@
/*******************************************************************************
* Copyright (c) 2023 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.data;
/**
* Types of predicate keywords Spring JPA repository method names
* @author danthe1st
*/
enum DataRepositoryMethodKeywordType {
/**
* A keyword that terminates an expression.
*
* e.g. {@code isTrue} in {@code findBySomeBooleanIsTrue}
*/
TERMINATE_EXPRESSION,
/**
* An operator combining two conditions.
*
* e.g. {@code And} in {@code findBySomeAttributeAndAnotherAttribute}
*/
COMBINE_CONDITIONS,
/**
* A keyword requiring an expression on both sides or an expression on one side and a parameter.
*
* e.g. {@code Equals} in {@code findBySomeAttributeEquals} or {@code findBySomeAttributeEqualsAnotherAttribute}
*/
COMPARE,
/**
* Keywords that can be ignored for content assist.
*
* e.g. {@code Not} in {@code findByNotSomeBooleanAttribute}
*/
IGNORE;
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* Copyright (c) 2023 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.data;
import java.util.List;
import java.util.Set;
/**
* Represents the result of parsing a Spring JPA repository query method
* @author danthe1st
*/
record DataRepositoryMethodNameParseResult(
/**
* Information about the subject of the method
*/
QueryMethodSubject subjectType,
/**
* parameters required for calling the method
*/
List<String> parameters,
/**
* {@code true} if the whole method shall be replaced including parameters, else false
*/
boolean performFullCompletion,
/**
* the last entered word, which completion options should be used for completing the expression.
*
* e.g. {@code First} in {@code findByFirst} which could be completed to {@code findByFirstName}
*/
String lastWord,
/**
* types of keywords that can be completed with
*/
Set<DataRepositoryMethodKeywordType> allowedKeywordTypes) {
}

View File

@@ -0,0 +1,200 @@
/*******************************************************************************
* Copyright (c) 2023 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.data;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Class responsible for parsing Spring JPA Repository query methods.
* @author danthe1st
*/
class DataRepositoryMethodParser {
private static final Map<String, List<QueryPredicateKeywordInfo>> PREDICATE_KEYWORDS_GROUPED_BY_FIRST_WORD = QueryPredicateKeywordInfo.PREDICATE_KEYWORDS
.stream()
.collect(Collectors.groupingBy(info->{
return findFirstWord(info.keyword());
}));
private final String prefix;
private final Map<String, List<DomainProperty>> propertiesGroupedByFirstWord;
private String expectedNextType = null;//the expected type as string if a type is expected, if the type cannot be found, the user should supply it
private boolean performFullCompletion = true;//if some invalid text is detected, do not complete the whole method
private String previousExpression = null;
public DataRepositoryMethodParser(String localPrefix, DataRepositoryDefinition repoDef) {
prefix = localPrefix;
propertiesGroupedByFirstWord = groupPropertiesByFirstWord(repoDef);
}
private Map<String, List<DomainProperty>> groupPropertiesByFirstWord(DataRepositoryDefinition repoDef) {
Map<String, List<DomainProperty>> grouped = new HashMap<>();
for(DomainProperty property : repoDef.getDomainType().getProperties()){
String firstWord = findFirstWord(property.getName());
grouped.putIfAbsent(firstWord, new ArrayList<>());
grouped.get(firstWord).add(property);
}
return grouped;
}
DataRepositoryMethodNameParseResult parseLocalPrefixForCompletion() {
int subjectPredicateSplitIndex = prefix.indexOf("By");
if (subjectPredicateSplitIndex == -1) {
return null;
}
QueryMethodSubject subjectType = parseSubject(subjectPredicateSplitIndex);
if (subjectType == null) {
return null;
}
String predicate = prefix.substring(subjectPredicateSplitIndex + 2);
List<String> parameters=new ArrayList<>();
parsePredicate(predicate, parameters);
EnumSet<DataRepositoryMethodKeywordType> allowedKeywordTypes = findAllowedKeywordTypesAtEnd();
return new DataRepositoryMethodNameParseResult(subjectType, parameters, performFullCompletion, previousExpression, allowedKeywordTypes);
}
private void parsePredicate(String predicate, List<String> parameters) {
int lastWordEnd = 0;
for (int i = 1; i <= predicate.length(); i++) {
if(i == predicate.length() || Character.isUpperCase(predicate.charAt(i))) {//word ends on uppercase letter or end of string
String word = predicate.substring(lastWordEnd, i);
QueryPredicateKeywordInfo keyword = findByLargestFirstWord(PREDICATE_KEYWORDS_GROUPED_BY_FIRST_WORD, QueryPredicateKeywordInfo::keyword, predicate, lastWordEnd, word);
if (keyword == null){
DomainProperty preferredWord = findByLargestFirstWord(propertiesGroupedByFirstWord, DomainProperty::getName, predicate, lastWordEnd, word);
if (preferredWord != null) {
i += preferredWord.getName().length() - word.length();
word=preferredWord.getName();
}
parseNonKeyword(word);
} else {
i += keyword.keyword().length() - word.length();
parseKeyword(parameters, keyword);
}
lastWordEnd = i;
}
}
if (expectedNextType != null) {
parameters.add(expectedNextType);
}
}
private QueryMethodSubject parseSubject(int subjectPredicateSplitIndex) {
String subject = prefix.substring(0, subjectPredicateSplitIndex);
QueryMethodSubject subjectType = null;
for(QueryMethodSubject queryMethodSubject : QueryMethodSubject.QUERY_METHOD_SUBJECTS){
if(subject.startsWith(queryMethodSubject.key())) {
subjectType = queryMethodSubject;
}
}
return subjectType;
}
private EnumSet<DataRepositoryMethodKeywordType> findAllowedKeywordTypesAtEnd() {
EnumSet<DataRepositoryMethodKeywordType> allowedKeywordTypes = EnumSet.allOf(DataRepositoryMethodKeywordType.class);
if (expectedNextType == null) {
allowedKeywordTypes.remove(DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION);
allowedKeywordTypes.remove(DataRepositoryMethodKeywordType.COMPARE);
}
return allowedKeywordTypes;
}
private void parseNonKeyword(String word) {
if (previousExpression == null) {
previousExpression = word;
//non-keywords just invert the status
//if an expression is expected, the word is the expression
//if not, some expression is required after the word
if (expectedNextType == null) {
expectedNextType = word;
} else {
expectedNextType = null;
}
} else {
//combine multiple words that are not keywords
previousExpression += word;
if (expectedNextType != null) {
expectedNextType = previousExpression;
}
}
}
private void parseKeyword(List<String> parameters, QueryPredicateKeywordInfo keyword) {
switch(keyword.type()) {
case TERMINATE_EXPRESSION: {//e.g. IsTrue
if (expectedNextType == null) {
//if no next type/expression is expected (which should not happen), do not complete the full method (parameters)
performFullCompletion = false;
}
expectedNextType = null;
break;
}
case COMBINE_CONDITIONS: {//e.g. And
//if an expression is expected, it is added to the parameters
if (expectedNextType != null) {
parameters.add(expectedNextType);
}
expectedNextType = null;
break;
}
case COMPARE: {//e.g. GreaterThan
if (expectedNextType == null) {
//nothing to compare, e.g. And directly followed by GreaterThan
performFullCompletion = false;
}
expectedNextType = previousExpression;
break;
}
case IGNORE:{
//ignore
break;
}
default:
throw new IllegalArgumentException("Unexpected value: " + keyword.type());
}
previousExpression = null;
}
private <T> T findByLargestFirstWord(Map<String, List<T>> toSearch, Function<T, String> expressionExtractor, String predicate, int lastWordEnd, String word) {
T ret = null;
if (toSearch.containsKey(word)) {
for(T possibleKeyword : toSearch.get(word)){
int endPosition = lastWordEnd + expressionExtractor.apply(possibleKeyword).length();
if (predicate.length() >= endPosition
&& expressionExtractor.apply(possibleKeyword).equals(predicate.substring(lastWordEnd, endPosition))
&& (ret == null || expressionExtractor.apply(possibleKeyword).length() > expressionExtractor.apply(possibleKeyword).length())) {//find largest valid keyword
ret = possibleKeyword;
}
}
}
return ret;
}
private static String findFirstWord(String expression) {
int firstWordEnd;
for (firstWordEnd = 1;
firstWordEnd < expression.length()
&& Character.isLowerCase(expression.charAt(firstWordEnd));
firstWordEnd++) {
//search is done in loop condition
}
return expression.substring(0, firstWordEnd);
}
}

View File

@@ -0,0 +1,182 @@
/*******************************************************************************
* Copyright (c) 2023 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.data;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.util.StringUtils;
/**
* This utility class provides content assist proposals for Spring JPA query methods.
* @author danthe1st
*/
class DataRepositoryPrefixSensitiveCompletionProvider {
private DataRepositoryPrefixSensitiveCompletionProvider() {
//prevent instantiation
}
static void addPrefixSensitiveProposals(Collection<ICompletionProposal> completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repoDef) {
String localPrefix = findLastJavaIdentifierPart(prefix);
if (localPrefix == null) {
return;
}
DataRepositoryMethodNameParseResult parseResult = new DataRepositoryMethodParser(localPrefix, repoDef).parseLocalPrefixForCompletion();
if(parseResult != null && parseResult.performFullCompletion()){
Map<String, DomainProperty> propertiesByName = getPropertiesByName(repoDef.getDomainType().getProperties());
addMethodCompletionProposal(completions, offset, repoDef, localPrefix, prefix, parseResult, propertiesByName);
if (parseResult.lastWord() == null || !propertiesByName.containsKey(parseResult.lastWord())) {
addPropertyProposals(completions, offset, repoDef, parseResult);
}
}
addQueryStartProposals(completions, localPrefix, doc, offset);
}
private static void addQueryStartProposals(Collection<ICompletionProposal> completions, String prefix, IDocument doc, int offset) {
for(QueryMethodSubject queryMethodSubject : QueryMethodSubject.QUERY_METHOD_SUBJECTS){
String toInsert = queryMethodSubject.key() + "By";
if(toInsert.startsWith(prefix)||isOffsetAfterWhitespace(doc, offset)) {
completions.add(DataRepositoryCompletionProcessor.createProposal(offset, CompletionItemKind.Text, prefix, toInsert, toInsert));
}
}
}
private static boolean isOffsetAfterWhitespace(IDocument doc, int offset) {
try {
return offset > 0 && Character.isWhitespace(doc.getChar(offset-1));
}catch (BadLocationException e) {
return false;
}
}
private static void addPropertyProposals(Collection<ICompletionProposal> completions, int offset, DataRepositoryDefinition repoDef, DataRepositoryMethodNameParseResult parseResult) {
for(DomainProperty property : repoDef.getDomainType().getProperties()){
String lastWord = parseResult.lastWord();
if (lastWord == null) {
lastWord = "";
}
if (property.getName().startsWith(lastWord)) {
DocumentEdits edits = new DocumentEdits(null, false);
edits.replace(offset - lastWord.length(), offset, property.getName());
DocumentEdits additionalEdits = new DocumentEdits(null, false);
ICompletionProposal proposal = new FindByCompletionProposal(property.getName(), CompletionItemKind.Text, edits, "property " + property.getName(), null, Optional.of(additionalEdits), lastWord);
completions.add(proposal);
}
}
}
private static void addMethodCompletionProposal(Collection<ICompletionProposal> completions, int offset, DataRepositoryDefinition repoDef, String localPrefix, String fullPrefix, DataRepositoryMethodNameParseResult parseResult, Map<String, DomainProperty> propertiesByName) {
String methodName = localPrefix;
DocumentEdits edits = new DocumentEdits(null, false);
String signature = buildSignature(methodName, propertiesByName, parseResult);
StringBuilder newText = new StringBuilder();
newText.append(parseResult.subjectType().returnType());
if (parseResult.subjectType().isTyped()) {
newText.append("<");
newText.append(repoDef.getDomainType().getSimpleName());
newText.append(">");
}
String returnType = newText.toString();
newText.append(" ");
newText.append(signature);
newText.append(";");
int replaceStart = calculateReplaceOffset(offset, localPrefix, fullPrefix, returnType);
edits.replace(replaceStart, offset, newText.toString());
DocumentEdits additionalEdits = new DocumentEdits(null, false);
ICompletionProposal proposal = new FindByCompletionProposal(methodName, CompletionItemKind.Method, edits, null, null, Optional.of(additionalEdits), signature);
completions.add(proposal);
}
private static int calculateReplaceOffset(int offset, String localPrefix, String fullPrefix, String returnType) {
int replaceStart = offset - localPrefix.length();
String beforeLocalPrefix = fullPrefix.substring(0, fullPrefix.length()-localPrefix.length());
String trimmed = beforeLocalPrefix.trim();
if(trimmed.endsWith(returnType)) {
replaceStart -= (beforeLocalPrefix.length() - trimmed.length()) + returnType.length();
}
return replaceStart;
}
private static Map<String, DomainProperty> getPropertiesByName(DomainProperty[] properties) {
Map<String, DomainProperty> propertiesByName = new HashMap<>();
for(DomainProperty prop : properties){
propertiesByName.put(prop.getName(), prop);
}
return propertiesByName;
}
private static String buildSignature(String methodName, Map<String, DomainProperty> properties, DataRepositoryMethodNameParseResult parseResult) {
StringBuilder signatureBuilder = new StringBuilder();
signatureBuilder.append(methodName);
signatureBuilder.append("(");
List<String> parameters = parseResult.parameters();
for(int i = 0; i < parameters.size(); i++){
String param = parameters.get(i);
DomainType type = findExpressionType(properties, param);
if (type == null) {
signatureBuilder.append("Object");
}else {
signatureBuilder.append(type.getSimpleName());
}
signatureBuilder.append(" ");
signatureBuilder.append(StringUtils.uncapitalize(param));
if (i + 1 < parameters.size()) {
signatureBuilder.append(", ");
}
}
signatureBuilder.append(")");
return signatureBuilder.toString();
}
private static DomainType findExpressionType(Map<String, DomainProperty> properties, String param) {
String[] splitByUnderscore = param.split("_");
if(properties.containsKey(splitByUnderscore[0])) {
DomainType type = properties.get(splitByUnderscore[0]).getType();
for (int j = 1; j < splitByUnderscore.length && type != null; j++) {
type = findMatchingParameter(splitByUnderscore[j], type);
}
return type;
}
return null;
}
private static DomainType findMatchingParameter(String name, DomainType type) {
for(DomainProperty prop : type.getProperties()){
if (prop.getName().equals(name)) {
return prop.getType();
}
}
return null;
}
private static String findLastJavaIdentifierPart(String prefix) {
if (prefix == null) {
return null;
}
int lastNonIdentifierPartIndex;
for (lastNonIdentifierPartIndex = prefix.length() - 1; lastNonIdentifierPartIndex >= 0 && Character.isJavaIdentifierPart(prefix.charAt(lastNonIdentifierPartIndex)); lastNonIdentifierPartIndex--) {
// search done using loop condition
}
return prefix.substring(lastNonIdentifierPartIndex + 1);
}
}

View File

@@ -36,7 +36,11 @@ public class DomainType {
}
public DomainType(ITypeBinding typeBinding) {
this.packageName = typeBinding.getPackage().getName();
if (typeBinding.getPackage() == null) {
this.packageName = "";
} else {
this.packageName = typeBinding.getPackage().getName();
}
this.fullName = typeBinding.getQualifiedName();
this.simpleName = typeBinding.getName();
@@ -48,9 +52,17 @@ public class DomainType {
for (IMethodBinding method : methods) {
String methodName = method.getName();
if (methodName != null && methodName.startsWith("get")) {
String propertyName = methodName.substring(3);
properties.add(new DomainProperty(propertyName, new DomainType(method.getReturnType())));
if (methodName != null) {
String propertyName = null;
if (methodName.startsWith("get")) {
propertyName = methodName.substring(3);
}
else if (methodName.startsWith("is")) {
propertyName = methodName.substring(2);
}
if (propertyName != null) {
properties.add(new DomainProperty(propertyName, new DomainType(method.getReturnType())));
}
}
}
return (DomainProperty[]) properties.toArray(new DomainProperty[properties.size()]);

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* Copyright (c) 2023 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.data;
import java.util.List;
/**
* Represents information about the subject of a JPA query method.
*
* See https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#appendix.query.method.subject
* @author danthe1st
*/
record QueryMethodSubject(
String key, String returnType, boolean isTyped) {
static final List<QueryMethodSubject> QUERY_METHOD_SUBJECTS = List.of(
QueryMethodSubject.createCollectionSubject("find", "List"),
QueryMethodSubject.createCollectionSubject("read", "List"),
QueryMethodSubject.createCollectionSubject("get", "List"),
QueryMethodSubject.createCollectionSubject("query", "List"),
QueryMethodSubject.createCollectionSubject("search", "List"),
QueryMethodSubject.createCollectionSubject("stream", "Streamable"),
QueryMethodSubject.createPrimitiveSubject("exists", "boolean"),
QueryMethodSubject.createPrimitiveSubject("count", "long"),
QueryMethodSubject.createPrimitiveSubject("delete", "void"),
QueryMethodSubject.createPrimitiveSubject("remove", "void")
);
private static QueryMethodSubject createPrimitiveSubject(String key, String primitive) {
return new QueryMethodSubject(key, primitive, false);
}
private static QueryMethodSubject createCollectionSubject(String key, String collectionType) {
return new QueryMethodSubject(key, collectionType, true);
}
}

View File

@@ -0,0 +1,87 @@
/*******************************************************************************
* Copyright (c) 2023 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.data;
import java.util.List;
/**
* Represents information about the predicate in Spring JPA repository query methods.
*
* See https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#appendix.query.method.predicate
*
* @author danthe1st
*/
record QueryPredicateKeywordInfo(String keyword, DataRepositoryMethodKeywordType type) {
static final List<QueryPredicateKeywordInfo> PREDICATE_KEYWORDS = List.of(
new QueryPredicateKeywordInfo("And", DataRepositoryMethodKeywordType.COMBINE_CONDITIONS),
new QueryPredicateKeywordInfo("Or", DataRepositoryMethodKeywordType.COMBINE_CONDITIONS),
new QueryPredicateKeywordInfo("After", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsAfter", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Before", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsBefore", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Containing", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsContaining", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Contains", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Between", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsBetween", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("EndingWith", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsEndingWith", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("EndsWith", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Exists", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("False", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("IsFalse", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("GreaterThan", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsGreaterThan", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("GreaterThanEqual", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsGreaterThanEqual", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("In", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsIn", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Is", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Equals", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Empty", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("IsEmpty", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("NotEmpty", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("IsNotEmpty", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("NotNull", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("IsNotNull", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("Null", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("IsNull", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("LessThan", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsLessThan", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("LessThanEqual", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsLessThanEqual", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Like", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsLike", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Near", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsNear", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Not", DataRepositoryMethodKeywordType.IGNORE),
new QueryPredicateKeywordInfo("IsNot", DataRepositoryMethodKeywordType.IGNORE),
new QueryPredicateKeywordInfo("NotIn", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsNotIn", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("NotLike", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsNotLike", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Regex", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("MatchesRegex", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("Matches", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("StartingWith", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsStartingWith", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("StartsWith", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("True", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("IsTrue", DataRepositoryMethodKeywordType.TERMINATE_EXPRESSION),
new QueryPredicateKeywordInfo("Within", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IsWithin", DataRepositoryMethodKeywordType.COMPARE),
new QueryPredicateKeywordInfo("IgnoreCase", DataRepositoryMethodKeywordType.IGNORE),
new QueryPredicateKeywordInfo("IgnoringCase", DataRepositoryMethodKeywordType.IGNORE),
new QueryPredicateKeywordInfo("AllIgnoreCase", DataRepositoryMethodKeywordType.IGNORE),
new QueryPredicateKeywordInfo("AllIgnoringCase", DataRepositoryMethodKeywordType.IGNORE),
new QueryPredicateKeywordInfo("OrderBy", DataRepositoryMethodKeywordType.COMBINE_CONDITIONS)
);
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2018 Pivotal, Inc.
* Copyright (c) 2018, 2023 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
@@ -11,6 +11,7 @@
package org.springframework.ide.vscode.boot.java.data.test;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.IOUtils;
@@ -27,13 +28,13 @@ import org.springframework.ide.vscode.boot.bootiful.HoverTestConf;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
import org.springframework.ide.vscode.languageserver.testharness.TestAsserts;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Martin Lippert
* @author danthe1st
*/
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@@ -53,11 +54,100 @@ public class DataRepositoryCompletionProcessorTest {
@Test
void testStandardFindByCompletions() throws Exception {
prepareCase("{", "{<*>");
assertContainsAnnotationCompletions(
"List<Customer> findByFirstName(String firstName);",
"List<Customer> findByLastName(String lastName);");
assertStandardCompletions();
}
@Test
void testPrefixSensitiveCompletionsNoPrefix() throws Exception {
prepareCase("{\n}", "{\n<*>");
assertStandardCompletions();
}
private void assertStandardCompletions() throws Exception {
assertContainsAnnotationCompletions(
"countBy",
"deleteBy",
"existsBy",
"findBy",
"List<Customer> findByFirstName(String firstName);",
"List<Customer> findById(Long id);",
"List<Customer> findByLastName(String lastName);",
"List<Customer> findByResponsibleEmployee(Employee responsibleEmployee);",
"getBy",
"queryBy",
"readBy",
"removeBy",
"searchBy",
"streamBy");
}
@Test
void testPrefixSensitiveCompletionsCompleteMethod() throws Exception {
checkCompletions("findByFirstNameAndLastName", "List<Customer> findByFirstNameAndLastName(String firstName, String lastName);");
}
@Test
void testPrefixSensitiveCompletionsCompleteMethodReturnTypePresent() throws Exception {
checkCompletions("List<Customer> findByFirstNameAndLastName", "List<Customer> findByFirstNameAndLastName(String firstName, String lastName);");
checkCompletions("boolean existsByFirstNameAndLastName", "boolean existsByFirstNameAndLastName(String firstName, String lastName);");
}
@Test
void testAttributeComparison() throws Exception {
checkCompletions("findByFirstNameIsGreaterThanLastName", "List<Customer> findByFirstNameIsGreaterThanLastName();");
checkCompletions("findByFirstNameIsLastName", "List<Customer> findByFirstNameIsLastName();");
}
@Test
void testTerminatingKeyword() throws Exception {
checkCompletions("findByFirstNameNull", "List<Customer> findByFirstNameNull();");
checkCompletions("findByFirstNameNotNull", "List<Customer> findByFirstNameNotNull();");
}
@Test
void testNewConditionAfterTerminatedExpression() throws Exception {
checkCompletions("findByFirstNameNullAndLastName", "List<Customer> findByFirstNameNullAndLastName(String lastName);");
checkCompletions("findByNotFirstNameNullAndNotLastName", "List<Customer> findByNotFirstNameNullAndNotLastName(String lastName);");
}
@Test
void testDifferentSubjectTypes() throws Exception {
checkCompletions("existsByFirstName", "boolean existsByFirstName(String firstName);");
checkCompletions("countByFirstName", "long countByFirstName(String firstName);");
checkCompletions("streamByFirstName", "Streamable<Customer> streamByFirstName(String firstName);");
checkCompletions("removeByFirstName", "void removeByFirstName(String firstName);");
}
@Test
void testUnknownAttribute() throws Exception {
checkCompletions("findByUnknownObject", "List<Customer> findByUnknownObject(Object unknownObject);");
}
@Test
void testKeywordInPredicate() throws Exception {
checkCompletions("findByThisCustomerIsSpecial", "List<Customer> findByThisCustomerIsSpecial(boolean thisCustomerIsSpecial);");
}
@Test
void testPropertyProposals() throws Exception {
checkCompletions("findByFirst", "findByFirstName");
checkCompletions("findByFirstNameAndL", "findByFirstNameAndLastName");
checkCompletions("findBy",
"findByFirstName",
"findByLastName");
}
@Test
void findByComplexExpression() throws Exception {
checkCompletions("findByResponsibleEmployee", "List<Customer> findByResponsibleEmployee(Employee responsibleEmployee);");
checkCompletions("findByResponsibleEmployee_SocialSecurityNumber", "List<Customer> findByResponsibleEmployee_SocialSecurityNumber(Long responsibleEmployee_SocialSecurityNumber);");
}
private void checkCompletions(String alredyPresent, String... expectedCompletions) throws Exception {
prepareCase("{\n}", "{\n\t" + alredyPresent + "<*>");
assertContainsAnnotationCompletions(Arrays.stream(expectedCompletions).map(expected -> "\t" + expected + "<*>").toArray(String[]::new));
}
private void prepareCase(String selectedAnnotation, String annotationStatementBeforeTest) throws Exception {
InputStream resource = this.getClass().getResourceAsStream("/test-projects/test-spring-data-symbols/src/main/java/org/test/TestCustomerRepositoryForCompletions.java");
String content = IOUtils.toString(resource);
@@ -74,13 +164,10 @@ public class DataRepositoryCompletionProcessorTest {
Editor clonedEditor = editor.clone();
clonedEditor.apply(foundCompletion);
if (clonedEditor.getText().contains(expectedResultsFromCompletion[i])) {
if (i < expectedResultsFromCompletion.length && clonedEditor.getText().contains(expectedResultsFromCompletion[i])) {
i++;
}
}
assertEquals(expectedResultsFromCompletion.length, i);
}
}

View File

@@ -2,7 +2,6 @@ package org.test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -19,18 +18,19 @@ public class Application {
@Bean
public CommandLineRunner demo(CustomerRepository repository) {
return (args) -> {
return args -> {
Employee employee = new Employee(1234, "Margot", "Al-Harazi");
// save a couple of customers
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
repository.save(new Customer("Jack", "Bauer", employee));
repository.save(new Customer("Chloe", "O'Brian", employee));
repository.save(new Customer("Kim", "Bauer", employee));
repository.save(new Customer("David", "Palmer", employee));
repository.save(new Customer("Michelle", "Dessler", employee));
// fetch all customers
log.info("Customers found with findAll():");
log.info("-------------------------------");
for (Customer customer : repository.findAll()) {
for(Customer customer : repository.findAll()){
log.info(customer.toString());
}
log.info("");
@@ -45,7 +45,7 @@ public class Application {
// fetch customers by last name
log.info("Customer found with findByLastName('Bauer'):");
log.info("--------------------------------------------");
for (Customer bauer : repository.findByLastName("Bauer")) {
for(Customer bauer : repository.findByLastName("Bauer")){
log.info(bauer.toString());
}
log.info("");

View File

@@ -5,29 +5,34 @@ import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private boolean thisCustomerIsSpecial;//contains keyword in name
protected Customer() {}
@ManyToOne
private Employee responsibleEmployee;
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
protected Customer() {}
@Override
public String toString() {
return String.format(
"Customer[id=%d, firstName='%s', lastName='%s']",
id, firstName, lastName);
}
public Customer(String firstName, String lastName, Employee responsibleEmployee) {
this.firstName = firstName;
this.lastName = lastName;
this.responsibleEmployee = responsibleEmployee;
}
@Override
public String toString() {
return "Customer [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", responsibleEmployee="
+ responsibleEmployee + "]";
}
// end::sample[]
@@ -42,5 +47,12 @@ public class Customer {
public String getLastName() {
return lastName;
}
}
public boolean isThisCustomerIsSpecial() {
return thisCustomerIsSpecial;
}
public Employee getResponsibleEmployee() {
return responsibleEmployee;
}
}

View File

@@ -0,0 +1,47 @@
// tag::sample[]
package org.test;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Employee {
@Id
private Long socialSecurityNumber;
private String firstName;
private String lastName;
protected Employee() {
}
public Employee(long socialSecurityNumber, String firstName, String lastName) {
this.socialSecurityNumber = socialSecurityNumber;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format(
"Employee[socialSecurityNumber=%d, firstName='%s', lastName='%s']",
socialSecurityNumber, firstName, lastName
);
}
// end::sample[]
public Long getSocialSecurityNumber() {
return socialSecurityNumber;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}

View File

@@ -1,7 +1,5 @@
package org.test;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface TestCustomerRepositoryForCompletions extends CrudRepository<Customer, Long> {