Skip to content

Commit

Permalink
Spring Data JPA Content Assist
Browse files Browse the repository at this point in the history
  • Loading branch information
danthe1st authored and BoykoAlex committed Feb 21, 2023
1 parent 4584be1 commit 97d6606
Show file tree
Hide file tree
Showing 13 changed files with 811 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@ public void provideCompletions(ASTNode node, int offset, IDocument doc, Collecti
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");
Expand All @@ -71,7 +74,6 @@ protected ICompletionProposal generateCompletionProposal(int offset, String pref
label.append(StringUtils.uncapitalize(domainProperty.getName()));
label.append(");");

DocumentEdits edits = new DocumentEdits(null, false);

StringBuilder completion = new StringBuilder();
completion.append("List<");
Expand All @@ -84,20 +86,25 @@ protected ICompletionProposal generateCompletionProposal(int offset, String pref
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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) {

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 97d6606

Please sign in to comment.