| /******************************************************************************* |
| * Copyright (c) 2011 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.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| 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.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.java.JavaEntity; |
| 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.0.1 |
| * @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) { |
| Status status = new Status(IStatus.ERROR, JptJpaUiPlugin.PLUGIN_ID, JptUiMessages.JpaJpqlJavaCompletionProposalComputer_Error, e); |
| JptJpaCorePlugin.log(status); |
| } |
| 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 = JptJpaCorePlugin.getJpaFile(file); |
| if (jpaFile == null) return Collections.emptyList(); |
| |
| monitor.worked(80); |
| checkCanceled(monitor); |
| |
| // Retrieve the JPA's model object |
| NamedQuery namedQuery = namedQuery(astRoot, 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, |
| CompilationUnit astRoot, |
| int tokenStart) { |
| |
| if (structureNode instanceof JavaPersistentType) { |
| JavaPersistentType persistentType = (JavaPersistentType) structureNode; |
| JavaTypeMapping typeMapping = persistentType.getMapping(); |
| |
| if (typeMapping instanceof JavaEntity) { |
| JavaEntity entity = (JavaEntity) typeMapping; |
| |
| for (JavaNamedQuery namedQuery : entity.getQueryContainer().getNamedQueries()) { |
| TextRange textRange = namedQuery.getQueryAnnotation().getQueryTextRange(astRoot); |
| |
| 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(CompilationUnit astRoot, JpaFile jpaFile, int tokenStart) { |
| |
| for (JpaStructureNode node : jpaFile.getRootStructureNodes()) { |
| NamedQuery namedQuery = findNamedQuery(node, astRoot, 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 |
| * @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; |
| ArrayInitializer array = (ArrayInitializer) singleMemberAnnotation.getValue(); |
| |
| for (org.eclipse.jdt.core.dom.Expression expression : (List<org.eclipse.jdt.core.dom.Expression>) array.expressions()) { |
| if (isInsideNode(expression, tokenStart, tokenEnd)) { |
| return retrieveQuery((NormalAnnotation) expression, 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; |
| } |
| } |