/*******************************************************************************
* Copyright (c) 2012 Spring IDE Developers
* 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Spring IDE Developers - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.data.jdt.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.SourceMethod;
import org.eclipse.jdt.internal.ui.SharedImages;
import org.eclipse.jdt.internal.ui.text.java.JavaCompletionProposal;
import org.eclipse.jdt.internal.ui.text.java.JavaCompletionProposalComputer;
import org.eclipse.jdt.ui.ISharedImages;
import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext;
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.swt.graphics.Image;
import org.springframework.ide.eclipse.data.internal.DataCoreImages;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springsource.ide.eclipse.commons.core.SpringCoreUtils;
/**
* Completion proposal computer to calculate Spring Data query keyword proposals and entity property proposals.
*
* @author Oliver Gierke
* @author Tomasz Zarna
*/
@SuppressWarnings("restriction")
public class EntityPropertyCompletionProposals extends JavaCompletionProposalComputer {
private static final ISharedImages IMAGES = new SharedImages();
private static final Image KEYWORD = DataCoreImages.getImage(DataCoreImages.IMG_OBJS_KEY);
@Override
public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context,
IProgressMonitor monitor) {
if (!(context instanceof JavaContentAssistInvocationContext)) {
return Collections.emptyList();
}
JavaContentAssistInvocationContext javaContext = (JavaContentAssistInvocationContext) context;
if (!SpringCoreUtils.isSpringProject(javaContext.getProject().getProject())) {
return Collections.emptyList();
}
if (javaContext.getCoreContext() == null) {
return Collections.emptyList();
}
ICompilationUnit cu = javaContext.getCompilationUnit();
try {
int invocationOffset = context.getInvocationOffset();
IJavaElement element = cu.getElementAt(invocationOffset);
if (element instanceof SourceMethod) {
return computeCompletionProposals((SourceMethod) element, javaContext);
} else {
if (element != null) {
IType type = (IType) element.getAncestor(IJavaElement.TYPE);
if (type != null) {
return computeCompletionProposals(type, javaContext);
}
}
}
} catch (JavaModelException e) {
}
return Collections.emptyList();
}
private List<ICompletionProposal> computeCompletionProposals(SourceMethod element,
JavaContentAssistInvocationContext javaContext) throws JavaModelException {
RepositoryInformation information = RepositoryInformation.create(element);
if (information == null) {
return Collections.emptyList();
}
int offset = javaContext.getCoreContext().getOffset();
int positionInMethodName = offset - element.getNameRange().getOffset();
String elementName = element.getElementName();
return computeCompletionProposals(javaContext, information, positionInMethodName, elementName);
}
private List<ICompletionProposal> computeCompletionProposals(IType type,
JavaContentAssistInvocationContext javaContext) throws JavaModelException {
RepositoryInformation information = RepositoryInformation.create(type);
if (information == null) {
return Collections.emptyList();
}
char[] token = javaContext.getCoreContext().getToken();
if (token == null) {
return Collections.emptyList();
}
String elementName = String.valueOf(token);
if (elementName.isEmpty()) {
return Collections.emptyList();
}
int offset = javaContext.getCoreContext().getOffset();
int positionInMethodName = offset - javaContext.getCoreContext().getTokenStart();
return computeCompletionProposals(javaContext, information, positionInMethodName, elementName);
}
private List<ICompletionProposal> computeCompletionProposals(
JavaContentAssistInvocationContext javaContext,
RepositoryInformation information, int positionInMethodName,
String elementName) throws JavaModelException {
IJavaProject project = javaContext.getProject();
Class<?> managedDomainClass = information.getManagedDomainClass();
if (managedDomainClass == null) {
return Collections.emptyList();
}
IType domainType = project.findType(managedDomainClass.getName());
int offset = javaContext.getCoreContext().getOffset();
KeywordProvider keywordProvider = information.getKeywordProvider(project);
QueryMethodCandidate candidate = new QueryMethodCandidate(elementName,
information.getManagedDomainClass());
QueryMethodPart part = candidate.getPartAtPosition(positionInMethodName);
if (part == null) {
return Collections.emptyList();
}
KeywordProposalsProvider keywordProposalsProvider = new KeywordProposalsProvider(keywordProvider);
IType type = part.isRoot() ? domainType : project.findType(part.getPathLeaf().getType().getName());
if (type == null) {
return Collections.emptyList();
}
List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
proposals.addAll(getProposalsFor(type, part, offset));
proposals.addAll(keywordProposalsProvider.getProposalsFor(type, offset, part));
return proposals;
}
private static List<ICompletionProposal> getProposalsFor(IType type, QueryMethodPart part, int offset)
throws JavaModelException {
if (isJdkType(type)) {
return Collections.emptyList();
}
List<ICompletionProposal> result = new ArrayList<ICompletionProposal>();
for (IField field : type.getFields()) {
if (part.isProposalCandidate(field)) {
result.add(new EntityFieldNameCompletionProposal(field, offset, part.getSeed()));
}
}
return result;
}
/**
* Code completion proposal for a query keyword.
*
* @author Oliver Gierke
*/
private static class KeyWordCompletionProposal extends JavaCompletionProposal {
public KeyWordCompletionProposal(String keyword, int offset, String seed) {
super(getReplacement(keyword, seed), offset, offset + keyword.length(), KEYWORD, StringUtils.capitalize(keyword),
450);
}
}
/**
* Provides {@link KeyWordCompletionProposal}s using a given {@link KeywordProvider}.
*
* @author Oliver Gierke
*/
private static class KeywordProposalsProvider {
private final KeywordProvider provider;
/**
* Creates a new {@link KeywordProposalsProvider} using the given {@link KeywordProposalsProvider}.
*
* @param provider must not be {@literal null}.
*/
public KeywordProposalsProvider(KeywordProvider provider) {
Assert.notNull(provider);
this.provider = provider;
}
public List<ICompletionProposal> getProposalsFor(IType type, int offset, QueryMethodPart part) {
if (part.isRoot()) {
return Collections.emptyList();
}
String seed = StringUtils.capitalize(part.isKeywordComplete() ? part.getKeyword() : part.getSeed());
List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for (String keyword : provider.getKeywordsForPropertyOf(type, seed)) {
if (!part.isKeywordComplete()) {
proposals.add(new KeyWordCompletionProposal(keyword, offset, seed));
}
}
seed = part.getSeed();
for (String concatenator : Arrays.asList("And", "Or")) {
if (part.isKeywordComplete() || !StringUtils.hasText(seed) || concatenator.startsWith(seed)) {
proposals.add(new KeyWordCompletionProposal(concatenator, offset, seed));
}
}
return proposals;
}
}
/**
* Returns whether the given {@link IType} is a JDK type.
*
* @param type must not be {@literal null}.
* @return
*/
private static boolean isJdkType(IType type) {
return type.getFullyQualifiedName().startsWith("java");
}
/**
* Code completion proposal for an entity field.
*
* @author Oliver Gierke
* @author Tomasz Zarna
*/
private static class EntityFieldNameCompletionProposal extends JavaCompletionProposal {
private static final Image DEFAULT_FIELD = IMAGES.getImage(ISharedImages.IMG_FIELD_DEFAULT);
private static final Image PRIVATE_FIELD = IMAGES.getImage(ISharedImages.IMG_FIELD_PRIVATE);
private static final Image PROTECTED_FIELD = IMAGES.getImage(ISharedImages.IMG_FIELD_PROTECTED);
private static final Image PUBLIC_FIELD = IMAGES.getImage(ISharedImages.IMG_FIELD_PUBLIC);
private String fieldName;
public EntityFieldNameCompletionProposal(IField field, int offset, String seed) {
super(getReplacement(field.getElementName(), seed), offset, offset + (seed == null ? 0 : seed.length()),
getFieldImage(field), field.getElementName(), 500);
this.fieldName = field.getElementName();
}
private static Image getFieldImage(IField field) {
try {
if (Flags.isPackageDefault(field.getFlags()))
return DEFAULT_FIELD;
if (Flags.isPrivate(field.getFlags()))
return PRIVATE_FIELD;
if (Flags.isProtected(field.getFlags()))
return PROTECTED_FIELD;
if (Flags.isPublic(field.getFlags()))
return PUBLIC_FIELD;
} catch (JavaModelException e) {
// ignore
}
return null;
}
@Override
public Object getAdditionalProposalInfo(IProgressMonitor monitor) {
return "findBy" + StringUtils.capitalize(fieldName);
}
}
/**
* Returns either the source capitalized if no seed is given or the part of the source starting at the length index of
* the seed.
*
* @param source must not be {@literal null} or shorter than the seed.
* @param seed can be {@literal null} or empty.
* @return
*/
private static String getReplacement(String source, String seed) {
return StringUtils.hasText(seed) ? source.substring(seed.length()) : StringUtils.capitalize(source);
}
}