package org.eclipse.jdt.postfixcompletion.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.NamingConventions;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.WildcardType;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.FieldReference;
import org.eclipse.jdt.internal.compiler.ast.MessageSend;
import org.eclipse.jdt.internal.compiler.ast.NameReference;
import org.eclipse.jdt.internal.compiler.lookup.ArrayBinding;
import org.eclipse.jdt.internal.compiler.lookup.BaseTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.ParameterizedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.VariableBinding;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.template.java.JavaContext;
import org.eclipse.jdt.internal.ui.text.correction.ASTResolving;
import org.eclipse.jdt.postfixcompletion.resolver.InnerExpressionResolver;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateBuffer;
import org.eclipse.jface.text.templates.TemplateContextType;
import org.eclipse.jface.text.templates.TemplateException;
import org.eclipse.jface.text.templates.TemplateVariable;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
/**
* This class is an extension to the existing {@link JavaContext} and includes/provides additional information
* on the current node which the code completion was invoked on.
* <br/>
* <br/>
* TODO Atm this class is dependent on non-published changes of the class {@link JavaContext}.
*/
@SuppressWarnings("restriction")
public class JavaStatementPostfixContext extends JavaContext {
private static final Object CONTEXT_TYPE_ID = "postfix"; //$NON-NLS-1$
private static final String OBJECT_SIGNATURE = "java.lang.Object"; //$NON-NLS-1$
private static final String ID_SEPARATOR = "��"; //$NON-NLS-1$
protected ASTNode currentCompletionNode;
protected ASTNode currentCompletionNodeParent;
protected Map<ASTNode, Region> nodeRegions;
protected ASTNode selectedNode;
private boolean domInitialized;
private BodyDeclaration bodyDeclaration;
private org.eclipse.jdt.core.dom.ASTNode parentDeclaration;
private Map<TemplateVariable, int[]> outOfRangeOffsets;
public JavaStatementPostfixContext(TemplateContextType type,
IDocument document, final int completionOffset, int completionLength,
ICompilationUnit compilationUnit) {
this(type, document, completionOffset, completionLength, compilationUnit, null, null);
}
public JavaStatementPostfixContext(TemplateContextType type,
IDocument document, Position completionPosition,
ICompilationUnit compilationUnit) {
this(type, document, completionPosition.getOffset(), completionPosition.getLength(), compilationUnit, null, null);
}
public JavaStatementPostfixContext(
TemplateContextType type,
IDocument document, int offset, int length,
ICompilationUnit compilationUnit,
ASTNode currentNode,
ASTNode parentNode) {
super(type, document, offset, length, compilationUnit);
this.nodeRegions = new HashMap<>();
this.currentCompletionNode = currentNode;
nodeRegions.put(currentNode, calculateNodeRegion(currentNode));
this.currentCompletionNodeParent = parentNode;
nodeRegions.put(parentNode, calculateNodeRegion(parentNode));
this.selectedNode = currentNode;
outOfRangeOffsets = new HashMap<>();
}
public String addImportGenericClass(String className) {
Pattern p = Pattern.compile("[a-zA-Z0-9$_\\.]+");
Matcher m = p.matcher(className);
List<String> classNames = new ArrayList<String>();
Map<String, String> classNameMapping = new HashMap<String, String>();
while (m.find()) {
classNames.add(className.substring(m.start(), m.end()));
}
/*
* In case the import class looks like this:
* a.b.c.Foo<b.c.Foo>
* we have to consider that - if we do not care about ordering, the following could happen:
* 1. trying to import b.c.Foo - import is resolved to Foo
* 2. replacing b.c.Foo with Foo - a.Foo<Foo> --> not correct (should be a.b.c.Foo<Foo>)
* 3. ...
*
* The solution to this is as follows:
* 1. sorting the fully qualified class names by length
* 2. replacing all occurring class names with unique identifiers ($$id$$)
* 3. importing all class names and map the fully qualified identifier with the resolved identifier of the class
* 4. replace the unique identifiers with the mapped values
*/
Collections.sort(classNames, new Comparator<String>() {
@Override
public int compare(String arg0, String arg1) {
return arg1.length() - arg0.length();
}
});
for (int i = 0; i < classNames.size(); i++) {
className = className.replace(classNames.get(i), ID_SEPARATOR + i + ID_SEPARATOR);
classNameMapping.put(classNames.get(i), addImport(classNames.get(i)));
}
for (int i = 0; i < classNames.size(); i++) {
className = className.replace(ID_SEPARATOR + i + ID_SEPARATOR, classNameMapping.get(classNames.get(i)));
}
return className;
}
private Region calculateNodeRegion(ASTNode node) {
if (node == null) {
return new Region(0, 0);
}
int start = getNodeBegin(node);
int end = getCompletionOffset() - getPrefixKey().length() - start - 1;
return new Region(start, end);
}
/*
* @see TemplateContext#canEvaluate(Template templates)
*/
@Override
public boolean canEvaluate(Template template) {
if (!template.getContextTypeId().equals(
JavaStatementPostfixContext.CONTEXT_TYPE_ID))
return false;
if (fForceEvaluation)
return true;
if (selectedNode == null) // We can evaluate to true only if we have a valid inner expression
return false;
if (template.getName().toLowerCase().startsWith(getPrefixKey().toLowerCase()) == false) {
return false;
}
// We check if the template makes "sense" by checking the requirements/conditions for the template
// For this purpose we have to resolve the inner_expression variable of the template
// This approach is much faster then delegating this to the existing TemplateTranslator class
String regex = ("\\$\\{([a-zA-Z]+):" + InnerExpressionResolver.INNER_EXPRESSION_VAR + "\\(([^\\$|\\{|\\}]*)\\)\\}"); // TODO Review this regex
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(template.getPattern());
boolean result = true;
while (matcher.find()) {
String[] types = matcher.group(2).split(",");
for (String s : types) {
if (!arrayContains(InnerExpressionResolver.FLAGS, s)) {
result = false;
if (this.isNodeResolvingTo(selectedNode, s.trim()) == true) {
return true;
}
}
}
}
return result;
}
private boolean arrayContains(Object[] array, Object o) {
for (Object a : array) {
if (a.equals(o)) return true;
}
return false;
}
/**
* Returns the current prefix of the key which was typed in.
* <br/>
* Examples:
* <code>
* <br/>
* new Object(). => getPrefixKey() returns ""<br/>
* new Object().a => getPrefixKey() returns "a"<br/>
* new object().asdf => getPrefixKey() returns "asdf"<br/>
*
* @return an empty string or a string which represents the prefix of the key which was typed in
*/
protected String getPrefixKey() {
IDocument document = getDocument();
int start = getCompletionOffset();
int end = getCompletionOffset();
try {
String temp = document.get(start, 1);
while (!".".equals(temp)) {
temp = document.get(--start, 1);
}
return document.get(start + 1, end - start - 1);
} catch (BadLocationException e) {
}
return "";
}
@Override
public int getEnd() {
return getCompletionOffset();
}
@Override
public int getStart() {
int result = super.getStart();
result -= getAffectedSourceRegion().getLength() + 1;
return result;
}
@Deprecated
public String getOuterExpression() {
return ""; // XXX This method is not used anymore
}
/**
* Calculates the beginning position of a given {@link ASTNode}
* @param node
* @return
*/
protected int getNodeBegin(ASTNode node) {
if (node instanceof NameReference) {
return ((NameReference) node).sourceStart;
} else if (node instanceof FieldReference) {
return ((FieldReference) node).receiver.sourceStart;
} else if (node instanceof MessageSend) {
return ((MessageSend) node).receiver.sourceStart;
}
return node.sourceStart;
}
/**
* Returns the {@link Region} which represents the source region of the affected statement.
* @return
*/
public Region getAffectedSourceRegion() {
return new Region(getCompletionOffset() - getPrefixKey().length() - nodeRegions.get(selectedNode).getLength() - 1, nodeRegions.get(selectedNode).getLength());
}
public String getAffectedStatement() {
Region r = getAffectedSourceRegion();
try {
return getDocument().get(r.getOffset(), r.getLength());
} catch (BadLocationException e) {
}
return "";
}
/**
* Returns <code>true</code> if the type or one of its supertypes of a given {@link ASTNode} resolves to a given type signature.
* <br/>
* Examples:
* <br/>
* <code>
* <br/>
* isNodeResolvingTo(node of type java.lang.String, "java.lang.Object") returns true<br/>
* isNodeResolvingTo(node of type java.lang.String, "java.lang.Iterable") returns false<br/>
* </code>
*
* TODO Implement this method without using the recursive helper method if there are any performance/stackoverflow issues
*
* @param node an ASTNode
* @param signature a fully qualified type
* @return true if the type of the given ASTNode itself or one of its superclass/superinterfaces resolves to the given signature. false otherwise.
*/
protected boolean isNodeResolvingTo(ASTNode node, String signature) {
if (signature == null || signature.trim().length() == 0) {
return true;
}
Binding b = resolveNodeToBinding(node);
if (b instanceof ParameterizedTypeBinding) {
ParameterizedTypeBinding ptb = (ParameterizedTypeBinding) b;
return resolvesReferenceBindingTo(ptb.actualType(), signature);
} else if (b instanceof BaseTypeBinding) {
return (new String(b.readableName()).equals(signature));
} else if (b instanceof TypeBinding) {
return resolvesReferenceBindingTo((TypeBinding) b, signature);
}
return true;
}
/**
* This is a recursive method which performs a depth first search in the inheritance graph of the given {@link TypeBinding}.
*
* @param sb a TypeBinding
* @param signature a fully qualified type
* @return true if the given TypeBinding itself or one of its superclass/superinterfaces resolves to the given signature. false otherwise.
*/
private boolean resolvesReferenceBindingTo(TypeBinding sb, String signature) {
if (sb == null) {
return false;
}
if (new String(sb.readableName()).startsWith(signature) || (sb instanceof ArrayBinding && "array".equals(signature))) {
return true;
}
List<ReferenceBinding> bindings = new ArrayList<>();
Collections.addAll(bindings, sb.superInterfaces());
bindings.add(sb.superclass());
boolean result = false;
Iterator<ReferenceBinding> it = bindings.iterator();
while (it.hasNext() && result == false) {
result = resolvesReferenceBindingTo(it.next(), signature);
}
return result;
}
protected boolean resolvesReferenceBindingToArray(TypeBinding sb) {
return sb instanceof ArrayBinding;
}
protected boolean isNodeOfBaseType(ASTNode node) {
return !isNodeResolvingTo(node, OBJECT_SIGNATURE);
}
protected boolean isNodeBooleanExpression(ASTNode node) {
return isNodeResolvingTo(node, "boolean");
}
protected Binding resolveNodeToBinding(ASTNode node) {
if (node instanceof NameReference) {
NameReference nr = (NameReference) node;
if (nr.binding instanceof VariableBinding) {
VariableBinding vb = (VariableBinding) nr.binding;
return vb.type;
}
} else if (node instanceof FieldReference) {
FieldReference fr = (FieldReference) node;
return fr.receiver.resolvedType;
}
return null;
}
protected String resolveNodeToTypeString(ASTNode node) {
Binding b = resolveNodeToBinding(node);
if (b != null) {
return new String(b.readableName());
}
return OBJECT_SIGNATURE;
}
/**
* Returns the fully qualified name the node of the current code completion invocation resolves to.
*
* @return a fully qualified type signature or the name of the base type.
*/
public String getInnerExpressionTypeSignature() {
return resolveNodeToTypeString(selectedNode);
}
/**
* Adds a new field to the {@link AST} using the given type and variable name. The method
* returns a {@link TextEdit} which can then be applied using the {@link #applyTextEdit(TextEdit)} method.
*
* @param type
* @param varName
* @return a {@link TextEdit} which represents the changes which would be made, or <code>null</code> if the field
* can not be created.
*/
public TextEdit addField(String type, String varName, boolean publicField, boolean staticField, boolean finalField, String value) {
if (isReadOnly())
return null;
if (!domInitialized)
initDomAST();
boolean isStatic = isBodyStatic();
int modifiers = (!publicField) ? Modifier.PRIVATE : Modifier.PUBLIC;
if (isStatic || staticField) {
modifiers |= Modifier.STATIC;
}
if (finalField) {
modifiers |= Modifier.FINAL;
}
ASTRewrite rewrite= ASTRewrite.create(parentDeclaration.getAST());
VariableDeclarationFragment newDeclFrag = addFieldDeclaration(rewrite, parentDeclaration, modifiers, varName, type, value);
TextEdit te = rewrite.rewriteAST(getDocument(), null);
return te;
}
private boolean isBodyStatic() {
boolean isAnonymous = parentDeclaration.getNodeType() == org.eclipse.jdt.core.dom.ASTNode.ANONYMOUS_CLASS_DECLARATION;
boolean isStatic = Modifier.isStatic(bodyDeclaration.getModifiers()) && !isAnonymous;
return isStatic;
}
private void initDomAST() {
if (isReadOnly())
return;
ASTParser parser= ASTParser.newParser(AST.JLS8);
parser.setSource(getCompilationUnit());
parser.setResolveBindings(true);
org.eclipse.jdt.core.dom.ASTNode domAst = parser.createAST(new NullProgressMonitor());
// org.eclipse.jdt.core.dom.AST ast = domAst.getAST();
NodeFinder nf = new NodeFinder(domAst, getCompletionOffset(), 1);
org.eclipse.jdt.core.dom.ASTNode cv = nf.getCoveringNode();
bodyDeclaration = ASTResolving.findParentBodyDeclaration(cv);
parentDeclaration = ASTResolving.findParentType(cv);
domInitialized = true;
}
/**
* Applies a {@link TextEdit} to the {@link IDocument} of this context and updates
* the completion offset variable.
*
* @param te {@link TextEdit} to apply
* @return <code>true</code> if the method was successful, <code>false</code> otherwise
*/
public boolean applyTextEdit(TextEdit te) {
try {
te.apply(getDocument());
setCompletionOffset(getCompletionOffset() + ((te.getOffset() < getCompletionOffset()) ? te.getLength() : 0));
return true;
} catch (MalformedTreeException | BadLocationException e) {
}
return false;
}
private VariableDeclarationFragment addFieldDeclaration(ASTRewrite rewrite, org.eclipse.jdt.core.dom.ASTNode newTypeDecl, int modifiers, String varName, String qualifiedName, String value) {
ChildListPropertyDescriptor property = ASTNodes.getBodyDeclarationsProperty(newTypeDecl);
List<BodyDeclaration> decls = ASTNodes.getBodyDeclarations(newTypeDecl);
AST ast = newTypeDecl.getAST();
VariableDeclarationFragment newDeclFrag = ast.newVariableDeclarationFragment();
newDeclFrag.setName(ast.newSimpleName(varName));
Type type = createType(Signature.createTypeSignature(qualifiedName, true), ast);
if (value != null && value.trim().length() > 0) {
Expression e = createExpression(value);
Expression ne = (Expression) org.eclipse.jdt.core.dom.ASTNode.copySubtree(ast, e);
newDeclFrag.setInitializer(ne);
} else {
if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) {
newDeclFrag.setInitializer(ASTNodeFactory.newDefaultExpression(ast, type, 0));
}
}
FieldDeclaration newDecl = ast.newFieldDeclaration(newDeclFrag);
newDecl.setType(type);
newDecl.modifiers().addAll(ASTNodeFactory.newModifiers(ast, modifiers));
int insertIndex = findFieldInsertIndex(decls, getCompletionOffset(), modifiers);
rewrite.getListRewrite(newTypeDecl, property).insertAt(newDecl, insertIndex, null);
return newDeclFrag;
}
private Expression createExpression(String expr) {
ASTParser parser = ASTParser.newParser(AST.JLS8);
parser.setKind(ASTParser.K_EXPRESSION);
parser.setResolveBindings(true);
parser.setSource(expr.toCharArray());
org.eclipse.jdt.core.dom.ASTNode astNode = parser.createAST(new NullProgressMonitor());
return (Expression) astNode;
}
private Type createType(String typeSig, AST ast) {
int sigKind = Signature.getTypeSignatureKind(typeSig);
switch (sigKind) {
case Signature.BASE_TYPE_SIGNATURE:
return ast.newPrimitiveType(PrimitiveType.toCode(Signature.toString(typeSig)));
case Signature.ARRAY_TYPE_SIGNATURE:
Type elementType = createType(Signature.getElementType(typeSig), ast);
return ast.newArrayType(elementType, Signature.getArrayCount(typeSig));
case Signature.CLASS_TYPE_SIGNATURE:
String erasureSig = Signature.getTypeErasure(typeSig);
String erasureName = Signature.toString(erasureSig);
if (erasureSig.charAt(0) == Signature.C_RESOLVED) {
erasureName = addImport(erasureName);
}
Type baseType= ast.newSimpleType(ast.newName(erasureName));
String[] typeArguments = Signature.getTypeArguments(typeSig);
if (typeArguments.length > 0) {
ParameterizedType type = ast.newParameterizedType(baseType);
List argNodes = type.typeArguments();
for (int i = 0; i < typeArguments.length; i++) {
String curr = typeArguments[i];
if (containsNestedCapture(curr)) {
argNodes.add(ast.newWildcardType());
} else {
argNodes.add(createType(curr, ast));
}
}
return type;
}
return baseType;
case Signature.TYPE_VARIABLE_SIGNATURE:
return ast.newSimpleType(ast.newSimpleName(Signature.toString(typeSig)));
case Signature.WILDCARD_TYPE_SIGNATURE:
WildcardType wildcardType= ast.newWildcardType();
char ch = typeSig.charAt(0);
if (ch != Signature.C_STAR) {
Type bound= createType(typeSig.substring(1), ast);
wildcardType.setBound(bound, ch == Signature.C_EXTENDS);
}
return wildcardType;
case Signature.CAPTURE_TYPE_SIGNATURE:
return createType(typeSig.substring(1), ast);
}
return ast.newSimpleType(ast.newName("java.lang.Object"));
}
private boolean containsNestedCapture(String signature) {
return signature.length() > 1 && signature.indexOf(Signature.C_CAPTURE, 1) != -1;
}
private int findFieldInsertIndex(List<BodyDeclaration> decls, int currPos, int modifiers) {
for (int i = decls.size() - 1; i >= 0; i--) {
org.eclipse.jdt.core.dom.ASTNode curr = decls.get(i);
if (curr instanceof FieldDeclaration && currPos > curr.getStartPosition() + curr.getLength()
&& ((FieldDeclaration) curr).getModifiers() == modifiers) {
return i + 1;
}
}
return 0;
}
public String[] suggestFieldName(String type, String[] excludes, boolean staticField, boolean finalField) throws IllegalArgumentException {
int dim = 0;
while (type.endsWith("[]")) {
dim++;
type = type.substring(0, type.length() - 2);
}
IJavaProject project = getJavaProject();
int namingConventions = 0;
if (staticField && finalField) {
namingConventions = NamingConventions.VK_STATIC_FINAL_FIELD;
} else if (staticField && !finalField) {
namingConventions = NamingConventions.VK_STATIC_FIELD;
} else {
namingConventions = NamingConventions.VK_INSTANCE_FIELD;
}
if (project != null)
return StubUtility.getVariableNameSuggestions(namingConventions, project, type, dim, Arrays.asList(excludes), true);
return new String[] {Signature.getSimpleName(type).toLowerCase()};
}
public String[] suggestFieldName(String type, boolean finalField, boolean forceStatic) {
if (!domInitialized) {
initDomAST();
}
if (domInitialized) {
return suggestFieldName(type, ASTResolving.getUsedVariableNames(bodyDeclaration), (forceStatic) ? forceStatic : isBodyStatic(), finalField);
}
// If the dom is not initialized yet (template preview) we return a dummy name
return new String[] { "newField" };
}
public void registerOutOfRangeOffset(TemplateVariable var, int absoluteOffset) {
if (outOfRangeOffsets.get(var) == null) {
outOfRangeOffsets.put(var, new int[] { absoluteOffset });
} else {
int[] temp = outOfRangeOffsets.get(var);
int[] newArr = new int[temp.length + 1];
System.arraycopy(temp, 0, newArr, 0, temp.length);
newArr[temp.length] = absoluteOffset;
outOfRangeOffsets.put(var, newArr);
}
}
public int[] getVariableOutOfRangeOffsets(TemplateVariable variable) {
return outOfRangeOffsets.get(variable);
}
@Override
public TemplateBuffer evaluate(Template template)
throws BadLocationException, TemplateException {
TemplateBuffer result = super.evaluate(template);
// After the template buffer has been created we are able to add out of range offsets
// This is not possible beforehand as it will result in an exception!
for (TemplateVariable tv : result.getVariables()) {
int[] outOfRangeOffsets = this.getVariableOutOfRangeOffsets(tv);
if (outOfRangeOffsets != null && outOfRangeOffsets.length > 0) {
int[] offsets = tv.getOffsets();
int[] newOffsets = new int[outOfRangeOffsets.length + offsets.length];
System.arraycopy(offsets, 0, newOffsets, 0, offsets.length);
for (int i = 0; i < outOfRangeOffsets.length; i++) {
newOffsets[i + offsets.length] = outOfRangeOffsets[i]; // - getAffectedSourceRegion().getOffset();
}
tv.setOffsets(newOffsets);
}
}
return result;
}
}