Factor out static methods for Spring Data repository completions

This commit is contained in:
danthe1st
2023-02-24 20:12:09 +01:00
committed by aboyko
parent 75fcb63fd0
commit 64fabdfdd0
11 changed files with 211 additions and 109 deletions

View File

@@ -11,27 +11,38 @@
package org.springframework.ide.vscode.boot.java.data;
import java.util.Collection;
import java.util.Optional;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.boot.java.data.providers.DataRepositoryCompletionProvider;
import org.springframework.ide.vscode.boot.java.data.providers.DataRepositoryQueryStartCompletionProvider;
import org.springframework.ide.vscode.boot.java.data.providers.DataRepositoryStandardCompletionProvider;
import org.springframework.ide.vscode.boot.java.data.providers.prefixsensitive.DataRepositoryPrefixSensitiveCompletionProvider;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
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.ide.vscode.commons.util.text.IRegion;
import org.springframework.util.StringUtils;
/**
* @author Martin Lippert
*/
public class DataRepositoryCompletionProcessor implements CompletionProvider {
private List<DataRepositoryCompletionProvider> completionProviders;
public DataRepositoryCompletionProcessor() {
this.completionProviders = List.of(
new DataRepositoryStandardCompletionProvider(),
new DataRepositoryQueryStartCompletionProvider(),
new DataRepositoryPrefixSensitiveCompletionProvider()
);
}
@Override
public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type,
int offset, IDocument doc, Collection<ICompletionProposal> completions) {
@@ -41,72 +52,20 @@ public class DataRepositoryCompletionProcessor implements CompletionProvider {
public void provideCompletions(ASTNode node, int offset, IDocument doc, Collection<ICompletionProposal> completions) {
TypeDeclaration type = ASTUtils.findDeclaringType(node);
DataRepositoryDefinition repo = getDataRepositoryDefinition(type);
if (repo != null) {
DomainType domainType = repo.getDomainType();
if (domainType != null) {
String prefix = "";
try {
IRegion line = doc.getLineInformationOfOffset(offset);
prefix = doc.get(line.getOffset(), offset - line.getOffset()).trim();
} catch (BadLocationException e) {
// ignore if there is a problem computing the prefix, continue without prefix
}
DomainProperty[] properties = domainType.getProperties();
for (DomainProperty property : properties) {
completions.add(generateCompletionProposal(offset, prefix, repo, property));
}
DataRepositoryPrefixSensitiveCompletionProvider.addPrefixSensitiveProposals(completions, doc, offset, prefix, repo);
if(repo != null && repo.getDomainType() != null){
String prefix = "";
try {
IRegion line = doc.getLineInformationOfOffset(offset);
prefix = doc.get(line.getOffset(), offset - line.getOffset()).trim();
} catch (BadLocationException e) {
// ignore if there is a problem computing the prefix, continue without prefix
}
for(DataRepositoryCompletionProvider provider : completionProviders){
provider.addProposals(completions, doc, offset, prefix, repo);
}
}
}
protected ICompletionProposal generateCompletionProposal(int offset, String prefix, DataRepositoryDefinition repoDef, DomainProperty domainProperty) {
StringBuilder label = new StringBuilder();
label.append("findBy");
label.append(StringUtils.capitalize(domainProperty.getName()));
label.append("(");
label.append(domainProperty.getType().getSimpleName());
label.append(" ");
label.append(StringUtils.uncapitalize(domainProperty.getName()));
label.append(");");
StringBuilder completion = new StringBuilder();
completion.append("List<");
completion.append(repoDef.getDomainType().getSimpleName());
completion.append("> findBy");
completion.append(StringUtils.capitalize(domainProperty.getName()));
completion.append("(");
completion.append(domainProperty.getType().getSimpleName());
completion.append(" ");
completion.append(StringUtils.uncapitalize(domainProperty.getName()));
completion.append(");");
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.startsWith(prefix)) {
edits.replace(offset - prefix.length(), offset, completion);
filter = completion;
}
else {
edits.insert(offset, completion);
}
DocumentEdits additionalEdits = new DocumentEdits(null, false);
return new FindByCompletionProposal(label, completionItemKind, edits, null, null, Optional.of(additionalEdits), filter);
}
private DataRepositoryDefinition getDataRepositoryDefinition(TypeDeclaration type) {
if (type != null) {
ITypeBinding resolvedType = type.resolveBinding();

View File

@@ -39,6 +39,24 @@ public class FindByCompletionProposal implements ICompletionProposal {
this.filter = filter;
}
public 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.startsWith(prefix)) {
edits.replace(offset - prefix.length(), offset, completion);
filter = completion;
}
else {
edits.insert(offset, completion);
}
DocumentEdits additionalEdits = new DocumentEdits(null, false);
return new FindByCompletionProposal(label, completionItemKind, edits, null, null, Optional.of(additionalEdits), filter);
}
@Override
public String getLabel() {
return label;

View File

@@ -0,0 +1,27 @@
/*******************************************************************************
* 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.providers;
import java.util.Collection;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryDefinition;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.util.text.IDocument;
/**
* Responsible for creating proposals related to Spring Data repositories.
* @author danthe1st
*/
public interface DataRepositoryCompletionProvider {
void addProposals(Collection<ICompletionProposal> completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repo);
}

View File

@@ -0,0 +1,48 @@
/*******************************************************************************
* 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.providers;
import java.util.Collection;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryDefinition;
import org.springframework.ide.vscode.boot.java.data.FindByCompletionProposal;
import org.springframework.ide.vscode.boot.java.data.providers.prefixsensitive.DataRepositoryPrefixSensitiveCompletionProvider;
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;
/**
* This class creates text roposals for query method subjects, e.g. {@code countBy}.
* @author danthe1st
*/
public class DataRepositoryQueryStartCompletionProvider implements DataRepositoryCompletionProvider{
@Override
public void addProposals(Collection<ICompletionProposal> completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repo) {
String localPrefix = DataRepositoryPrefixSensitiveCompletionProvider.findLastJavaIdentifierPart(prefix);
for(QueryMethodSubject queryMethodSubject : QueryMethodSubject.QUERY_METHOD_SUBJECTS){
String toInsert = queryMethodSubject.key() + "By";
if(prefix == null || toInsert.startsWith(localPrefix)||isOffsetAfterWhitespace(doc, offset)) {
completions.add(FindByCompletionProposal.createProposal(offset, CompletionItemKind.Text, prefix, toInsert, toInsert));
}
}
}
private boolean isOffsetAfterWhitespace(IDocument doc, int offset) {
try {
return offset > 0 && Character.isWhitespace(doc.getChar(offset-1));
}catch (BadLocationException e) {
return false;
}
}
}

View File

@@ -0,0 +1,61 @@
/*******************************************************************************
* 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.providers;
import java.util.Collection;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryDefinition;
import org.springframework.ide.vscode.boot.java.data.DomainProperty;
import org.springframework.ide.vscode.boot.java.data.DomainType;
import org.springframework.ide.vscode.boot.java.data.FindByCompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.util.StringUtils;
/**
* Provides content assist proposals for querying by a single attribute in Spring Data repositories.
* @author Martin Lippert
*/
public class DataRepositoryStandardCompletionProvider implements DataRepositoryCompletionProvider {
public void addProposals(Collection<ICompletionProposal> completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repo) {
DomainType domainType = repo.getDomainType();
DomainProperty[] properties = domainType.getProperties();
for (DomainProperty property : properties) {
completions.add(generateCompletionProposal(offset, prefix, repo, property));
}
}
private ICompletionProposal generateCompletionProposal(int offset, String prefix, DataRepositoryDefinition repoDef, DomainProperty domainProperty) {
StringBuilder label = new StringBuilder();
label.append("findBy");
label.append(StringUtils.capitalize(domainProperty.getName()));
label.append("(");
label.append(domainProperty.getType().getSimpleName());
label.append(" ");
label.append(StringUtils.uncapitalize(domainProperty.getName()));
label.append(");");
StringBuilder completion = new StringBuilder();
completion.append("List<");
completion.append(repoDef.getDomainType().getSimpleName());
completion.append("> findBy");
completion.append(StringUtils.capitalize(domainProperty.getName()));
completion.append("(");
completion.append(domainProperty.getType().getSimpleName());
completion.append(" ");
completion.append(StringUtils.uncapitalize(domainProperty.getName()));
completion.append(");");
return FindByCompletionProposal.createProposal(offset, CompletionItemKind.Method, prefix, label.toString(), completion.toString());
}
}

View File

@@ -8,7 +8,7 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data;
package org.springframework.ide.vscode.boot.java.data.providers;
import java.util.List;
@@ -18,10 +18,10 @@ import java.util.List;
* See https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#appendix.query.method.subject
* @author danthe1st
*/
record QueryMethodSubject(
public record QueryMethodSubject(
String key, String returnType, boolean isTyped) {
static final List<QueryMethodSubject> QUERY_METHOD_SUBJECTS = List.of(
public static final List<QueryMethodSubject> QUERY_METHOD_SUBJECTS = List.of(
QueryMethodSubject.createCollectionSubject("find", "List"),
QueryMethodSubject.createCollectionSubject("read", "List"),
QueryMethodSubject.createCollectionSubject("get", "List"),

View File

@@ -8,7 +8,7 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data;
package org.springframework.ide.vscode.boot.java.data.providers.prefixsensitive;
/**
* Types of predicate keywords Spring JPA repository method names

View File

@@ -8,13 +8,15 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data;
package org.springframework.ide.vscode.boot.java.data.providers.prefixsensitive;
import java.util.List;
import java.util.Set;
import org.springframework.ide.vscode.boot.java.data.providers.QueryMethodSubject;
/**
* Represents the result of parsing a Spring JPA repository query method
* Represents the result of parsing a Spring Data repository query method
* @author danthe1st
*/
record DataRepositoryMethodNameParseResult(

View File

@@ -8,7 +8,7 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data;
package org.springframework.ide.vscode.boot.java.data.providers.prefixsensitive;
import java.util.ArrayList;
import java.util.EnumSet;
@@ -18,6 +18,10 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryDefinition;
import org.springframework.ide.vscode.boot.java.data.DomainProperty;
import org.springframework.ide.vscode.boot.java.data.providers.QueryMethodSubject;
/**
* Class responsible for parsing Spring JPA Repository query methods.
* @author danthe1st

View File

@@ -8,7 +8,7 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data;
package org.springframework.ide.vscode.boot.java.data.providers.prefixsensitive;
import java.util.Collection;
import java.util.HashMap;
@@ -17,23 +17,24 @@ import java.util.Map;
import java.util.Optional;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryDefinition;
import org.springframework.ide.vscode.boot.java.data.DomainProperty;
import org.springframework.ide.vscode.boot.java.data.DomainType;
import org.springframework.ide.vscode.boot.java.data.FindByCompletionProposal;
import org.springframework.ide.vscode.boot.java.data.providers.DataRepositoryCompletionProvider;
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.
* This class provides content assist proposals for Spring Data repository query methods.
* @author danthe1st
*/
class DataRepositoryPrefixSensitiveCompletionProvider {
public class DataRepositoryPrefixSensitiveCompletionProvider implements DataRepositoryCompletionProvider {
private DataRepositoryPrefixSensitiveCompletionProvider() {
//prevent instantiation
}
static void addPrefixSensitiveProposals(Collection<ICompletionProposal> completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repoDef) {
@Override
public void addProposals(Collection<ICompletionProposal> completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repoDef) {
String localPrefix = findLastJavaIdentifierPart(prefix);
if (localPrefix == null) {
return;
@@ -47,27 +48,9 @@ class DataRepositoryPrefixSensitiveCompletionProvider {
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) {
private void addPropertyProposals(Collection<ICompletionProposal> completions, int offset, DataRepositoryDefinition repoDef, DataRepositoryMethodNameParseResult parseResult) {
for(DomainProperty property : repoDef.getDomainType().getProperties()){
String lastWord = parseResult.lastWord();
if (lastWord == null) {
@@ -83,7 +66,7 @@ class DataRepositoryPrefixSensitiveCompletionProvider {
}
}
private static void addMethodCompletionProposal(Collection<ICompletionProposal> completions, int offset, DataRepositoryDefinition repoDef, String localPrefix, String fullPrefix, DataRepositoryMethodNameParseResult parseResult, Map<String, DomainProperty> propertiesByName) {
private 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);
@@ -105,7 +88,7 @@ class DataRepositoryPrefixSensitiveCompletionProvider {
completions.add(proposal);
}
private static int calculateReplaceOffset(int offset, String localPrefix, String fullPrefix, String returnType) {
private 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();
@@ -115,7 +98,7 @@ class DataRepositoryPrefixSensitiveCompletionProvider {
return replaceStart;
}
private static Map<String, DomainProperty> getPropertiesByName(DomainProperty[] properties) {
private Map<String, DomainProperty> getPropertiesByName(DomainProperty[] properties) {
Map<String, DomainProperty> propertiesByName = new HashMap<>();
for(DomainProperty prop : properties){
propertiesByName.put(prop.getName(), prop);
@@ -123,7 +106,7 @@ class DataRepositoryPrefixSensitiveCompletionProvider {
return propertiesByName;
}
private static String buildSignature(String methodName, Map<String, DomainProperty> properties, DataRepositoryMethodNameParseResult parseResult) {
private String buildSignature(String methodName, Map<String, DomainProperty> properties, DataRepositoryMethodNameParseResult parseResult) {
StringBuilder signatureBuilder = new StringBuilder();
signatureBuilder.append(methodName);
signatureBuilder.append("(");
@@ -146,7 +129,7 @@ class DataRepositoryPrefixSensitiveCompletionProvider {
return signatureBuilder.toString();
}
private static DomainType findExpressionType(Map<String, DomainProperty> properties, String param) {
private DomainType findExpressionType(Map<String, DomainProperty> properties, String param) {
String[] splitByUnderscore = param.split("_");
if(properties.containsKey(splitByUnderscore[0])) {
DomainType type = properties.get(splitByUnderscore[0]).getType();
@@ -158,7 +141,7 @@ class DataRepositoryPrefixSensitiveCompletionProvider {
return null;
}
private static DomainType findMatchingParameter(String name, DomainType type) {
private DomainType findMatchingParameter(String name, DomainType type) {
for(DomainProperty prop : type.getProperties()){
if (prop.getName().equals(name)) {
return prop.getType();
@@ -167,7 +150,7 @@ class DataRepositoryPrefixSensitiveCompletionProvider {
return null;
}
private static String findLastJavaIdentifierPart(String prefix) {
public static String findLastJavaIdentifierPart(String prefix) {
if (prefix == null) {
return null;
}

View File

@@ -8,7 +8,7 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data;
package org.springframework.ide.vscode.boot.java.data.providers.prefixsensitive;
import java.util.List;