blob: cf10b337bf50faa877fb618600e1575a379b2ce9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2012 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Oracle - initial API and implementation
*
******************************************************************************/
package org.eclipse.jpt.jpa.ui.internal.jpql;
import java.util.Collections;
import java.util.List;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.CompletionContext;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer;
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jpt.common.core.internal.utility.jdt.ASTTools;
import org.eclipse.jpt.common.core.utility.TextRange;
import org.eclipse.jpt.common.utility.internal.StringTools;
import org.eclipse.jpt.jpa.core.JpaFile;
import org.eclipse.jpt.jpa.core.JpaStructureNode;
import org.eclipse.jpt.jpa.core.JptJpaCorePlugin;
import org.eclipse.jpt.jpa.core.context.NamedQuery;
import org.eclipse.jpt.jpa.core.context.Query;
import org.eclipse.jpt.jpa.core.context.java.JavaNamedQuery;
import org.eclipse.jpt.jpa.core.context.java.JavaPersistentType;
import org.eclipse.jpt.jpa.core.context.java.JavaTypeMapping;
import org.eclipse.jpt.jpa.ui.JptJpaUiPlugin;
import org.eclipse.jpt.jpa.ui.internal.JptUiMessages;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.swt.graphics.Image;
/**
* This computer adds content assist support when it is invoked inside the query element of {@link
* javax.persistence.NamedQuery @NamedQuery}.
*
* @version 3.2
* @since 3.0
* @author Pascal Filion
*/
@SuppressWarnings("restriction")
public final class JpaJpqlJavaCompletionProposalComputer extends JpqlCompletionProposalComputer<ICompletionProposal>
implements IJavaCompletionProposalComputer {
/**
* Creates a new <code>JpaJpqlJavaCompletionProposalComputer</code>.
*/
public JpaJpqlJavaCompletionProposalComputer() {
super();
}
/**
* {@inheritDoc}
*/
@Override
ICompletionProposal buildProposal(String proposal,
String displayString,
String additionalInfo,
Image image,
int cursorOffset) {
return new JpqlCompletionProposal(
contentAssistProposals,
proposal,
displayString,
additionalInfo,
image,
namedQuery,
actualQuery,
jpqlQuery,
offset + 1, // +1 is to skip the opening "
position,
cursorOffset,
true
);
}
/**
* {@inheritDoc}
*/
public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context,
IProgressMonitor monitor) {
if (context instanceof JavaContentAssistInvocationContext) {
monitor.beginTask(null, 100);
try {
return computeCompletionProposals((JavaContentAssistInvocationContext) context, monitor);
}
catch (Exception e) {
JptJpaUiPlugin.log(JptUiMessages.JpaJpqlJavaCompletionProposalComputer_Error, e);
}
finally {
monitor.done();
}
}
return Collections.emptyList();
}
private List<ICompletionProposal> computeCompletionProposals(JavaContentAssistInvocationContext context,
IProgressMonitor monitor) throws Exception {
CompletionContext completionContext = context.getCoreContext();
if (completionContext == null) return Collections.emptyList();
// The token "start" is the offset of the token's first character within the document.
// A token start of -1 can means:
// - It is inside the string representation of a unicode character, \\u0|0E9 where | is the
// cursor, then -1 is returned;
// - The string is not valid (it has some invalid characters)
int tokenStart = completionContext.getTokenStart();
if (tokenStart == -1) return Collections.emptyList();
int[] position = { completionContext.getOffset() - tokenStart - 1 };
if (position[0] < 0) return Collections.emptyList();
ICompilationUnit compilationUnit = context.getCompilationUnit();
if (compilationUnit == null) return Collections.emptyList();
CompilationUnit astRoot = ASTTools.buildASTRoot(compilationUnit);
IFile file = getCorrespondingResource(compilationUnit);
if (file == null) return Collections.emptyList();
JpaFile jpaFile = (JpaFile) file.getAdapter(JpaFile.class);
if (jpaFile == null) return Collections.emptyList();
monitor.worked(80);
checkCanceled(monitor);
// Retrieve the JPA's model object
NamedQuery namedQuery = namedQuery(jpaFile, tokenStart);
if (namedQuery == null) return Collections.emptyList();
// Retrieve the actual value of the element "query" since the content assist can be
// invoked before the model received the new content
String jpqlQuery = jpqlQuery(astRoot, tokenStart, completionContext.getTokenEnd(), position);
// Now create the proposals
return buildProposals(namedQuery, jpqlQuery, tokenStart, position[0]);
}
/**
* {@inheritDoc}
*/
public List<IContextInformation> computeContextInformation(ContentAssistInvocationContext context,
IProgressMonitor monitor) {
return Collections.emptyList();
}
private NamedQuery findNamedQuery(JpaStructureNode structureNode,
int tokenStart) {
if (structureNode instanceof JavaPersistentType) {
JavaPersistentType persistentType = (JavaPersistentType) structureNode;
JavaTypeMapping typeMapping = persistentType.getMapping();
for (Query query : typeMapping.getQueries()){
if (query.getType().equals(NamedQuery.class)){
JavaNamedQuery namedQuery = (JavaNamedQuery)query;
TextRange textRange = namedQuery.getQueryAnnotation().getQueryTextRange();
if ((textRange != null) && textRange.includes(tokenStart)) {
return namedQuery;
}
}
}
}
return null;
}
private IFile getCorrespondingResource(ICompilationUnit compilationUnit) {
try {
return (IFile) compilationUnit.getCorrespondingResource();
}
catch (JavaModelException ex) {
JptJpaCorePlugin.log(ex);
return null;
}
}
private boolean isInsideNode(ASTNode node, int tokenStart, int tokenEnd) {
int startPosition = node.getStartPosition();
return startPosition <= tokenStart &&
startPosition + node.getLength() >= tokenEnd;
}
private String jpqlQuery(CompilationUnit astRoot, int tokenStart, int tokenEnd, int[] position) {
String jpqlQuery = retrieveQuery(astRoot, tokenStart, tokenEnd);
if (jpqlQuery == null) {
jpqlQuery = StringTools.EMPTY_STRING;
}
else if (StringTools.stringIsQuoted(jpqlQuery)) {
jpqlQuery = jpqlQuery.substring(1, jpqlQuery.length() - 1);
}
return jpqlQuery;
}
/**
* {@inheritDoc}
*/
@Override
String modifyJpqlQuery(String jpqlQuery, int[] position) {
return ExpressionTools.unescape(jpqlQuery, position);
}
private NamedQuery namedQuery(JpaFile jpaFile, int tokenStart) {
for (JpaStructureNode node : jpaFile.getRootStructureNodes()) {
NamedQuery namedQuery = findNamedQuery(node, tokenStart);
if (namedQuery != null) {
return namedQuery;
}
}
return null;
}
/**
* This twisted code is meant to retrieve the real string value that is not escaped and to also
* retrieve the position within the non-escaped string. The query could have escape characters,
* such as \r, \n etc being written as \\r, \\n, the position is based on that escaped string,
* the conversion will convert them into \r and \r and adjust the position accordingly.
*
* @param astRoot The parsed tree representation of the Java source file
* @param tokenStart The beginning of the query expression of the {@link javax.persistence.NamedQuery
* &#64;NamedQuery}'s query member within the source file
* @param tokenEnd The end of the query member within the source file
* @param position The position of the cursor within the query expression
* @return The actual value retrieved from the query element
*/
@SuppressWarnings("unchecked")
private String retrieveQuery(CompilationUnit astRoot, int tokenStart, int tokenEnd) {
// Dig into the TypeDeclarations
for (AbstractTypeDeclaration type : (List<AbstractTypeDeclaration>) astRoot.types()) {
if (isInsideNode(type, tokenStart, tokenEnd)) {
// Dig inside its modifiers and annotations
for (IExtendedModifier modifier : (List<IExtendedModifier>) type.modifiers()) {
if (!modifier.isAnnotation()) {
continue;
}
Annotation annotation = (Annotation) modifier;
// Dig inside the annotation
if (isInsideNode(annotation, tokenStart, tokenEnd)) {
// @NamedQueries({...})
if (annotation.isSingleMemberAnnotation()) {
SingleMemberAnnotation singleMemberAnnotation = (SingleMemberAnnotation) annotation;
Expression value = singleMemberAnnotation.getValue();
if (value.getNodeType() == ASTNode.ARRAY_INITIALIZER) {
ArrayInitializer array = (ArrayInitializer) value;
for (Expression expression : (List<Expression>) array.expressions()) {
if (isInsideNode(expression, tokenStart, tokenEnd)) {
return retrieveQuery((NormalAnnotation) expression, tokenStart, tokenEnd);
}
}
}
else {
NormalAnnotation childAnnotation = (NormalAnnotation) value;
if (isInsideNode(childAnnotation, tokenStart, tokenEnd)) {
return retrieveQuery(childAnnotation, tokenStart, tokenEnd);
}
}
}
// @NamedQuery()
else if (annotation.isNormalAnnotation()) {
return retrieveQuery((NormalAnnotation) annotation, tokenStart, tokenEnd);
}
}
}
}
}
return null;
}
@SuppressWarnings("unchecked")
private String retrieveQuery(NormalAnnotation annotation, int tokenStart, int tokenEnd) {
for (MemberValuePair pair : (List<MemberValuePair>) annotation.values()) {
org.eclipse.jdt.core.dom.Expression expression = pair.getValue();
if (isInsideNode(expression, tokenStart, tokenEnd)) {
StringLiteral literal = (StringLiteral) pair.getValue();
return literal.getEscapedValue();
}
}
return null;
}
}