/**
* Copyright (c) 2014 Codetrails GmbH.
* 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:
* Marcel Bruch - initial API and implementation.
*/
package org.eclipse.recommenders.jdt.templates;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.SystemUtils.LINE_SEPARATOR;
import static org.eclipse.recommenders.internal.jdt.l10n.LogMessages.*;
import static org.eclipse.recommenders.utils.Logs.log;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IPackageBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.recommenders.utils.Nullable;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
/**
* @see <a href=
* "http://help.eclipse.org/mars/index.jsp?topic=%2Forg.eclipse.jdt.doc.user%2Fconcepts%2Fconcept-template-variables.htm">
* Template variables</a>
*/
public class SnippetCodeBuilder {
private final CompilationUnit ast;
private final ASTNode startNode;
private final IDocument document;
private final IRegion textSelection;
private final Map<ASTNode, String> nodesToReplace;
private final Set<String> imports = new TreeSet<>();
private final Set<String> importStatics = new TreeSet<>();
private final HashMap<IVariableBinding, String> templateVariableNameReferences = new HashMap<>();
private final HashSet<String> assignedTemplateVariablesNames = new HashSet<>();
private final HashMap<String, Integer> lastTemplateVariableIndex = new HashMap<>();
private final StringBuilder code = new StringBuilder();
/**
* A convenience constructor calling
* {@link SnippetCodeBuilder#SnippetCodeBuilder(CompilationUnit, IDocument, IRegion, Map) with an empty map, i.e.,
* no nodes to replace.
*/
public SnippetCodeBuilder(CompilationUnit ast, IDocument document, IRegion textSelection) {
this(ast, document, textSelection, Collections.<ASTNode, String>emptyMap());
}
/**
* @param nodesToReplace
* a map whose keys are {@code ASTNode}s completely covered by {@code textSelection} which will be
* replaced by template variables. The map's values are the preferred template variable names for the
* corresponding nodes. A {@code ASTNode} will be replaced by
* <code>${variableName:var(typeOfExpression)}</code> if it is an {@link Expression} and by
* <code>${variableName}</code> otherwise.
*
* @since 2.2.6
*/
public SnippetCodeBuilder(CompilationUnit ast, IDocument document, IRegion textSelection,
Map<ASTNode, String> nodesToReplace) {
this(ast, ast, document, textSelection, nodesToReplace);
}
/**
* A convenience constructor calling {@link SnippetCodeBuilder#SnippetCodeBuilder(ASTNode, IDocument, IRegion, Map)
* with an empty map, i.e., no nodes to replace.
*/
public SnippetCodeBuilder(ASTNode startNode, IDocument document, IRegion textSelection) {
this(startNode, document, textSelection, Collections.<ASTNode, String>emptyMap());
}
/**
* @param startNode
* an {@code ASTNode} which <strong>must</strong> completely cover the {@code textSelection}. The closer
* the node covers the selection the better the performance of {@link SnippetCodeBuilder#build()} will
* be. If in doubt, use {@link #SnippetCodeBuilder(CompilationUnit, IDocument, IRegion)} to pass an
* entire {@code CompilationUnit}
* @param nodesToReplace
* a map whose keys are {@code ASTNode}s below {@code startNode} which will be replaced by template
* variables. The map's values are the preferred template variable names for the corresponding nodes. A
* {@code ASTNode} will be replaced by <code>${variableName:var(typeOfExpression)}</code> if it is an
* {@link Expression} and by <code>${variableName}</code> otherwise.
*
* @since 2.2.6
*/
public SnippetCodeBuilder(ASTNode startNode, IDocument document, IRegion textSelection,
Map<ASTNode, String> nodesToReplace) {
this((CompilationUnit) startNode.getRoot(), startNode, document, textSelection, nodesToReplace);
}
private SnippetCodeBuilder(CompilationUnit ast, ASTNode startNode, IDocument document, IRegion textSelection,
Map<ASTNode, String> nodesToReplace) {
this.ast = requireNonNull(ast);
this.startNode = requireNonNull(startNode);
this.document = requireNonNull(document);
this.textSelection = requireNonNull(textSelection);
this.nodesToReplace = requireNonNull(nodesToReplace);
}
public String build() {
final int start = textSelection.getOffset();
final int length = textSelection.getLength();
String text;
try {
text = document.get(start, length);
} catch (BadLocationException e) {
IJavaElement javaElement = ast.getJavaElement();
log(WARN_FAILED_TO_GET_TEXT_SELECTION, e, javaElement == null ? null : javaElement.getHandleIdentifier(),
start, length);
return "";
}
if (text == null) {
IJavaElement javaElement = ast.getJavaElement();
log(WARN_FAILED_TO_GET_TEXT_SELECTION, javaElement == null ? null : javaElement.getHandleIdentifier(),
start, length);
return ""; //$NON-NLS-1$
}
final char[] chars = text.toCharArray();
final ASTNode enclosingNode = NodeFinder.perform(startNode, start, length);
outer: for (int i = 0; i < chars.length; i++) {
int offset = start + i;
for (Entry<ASTNode, String> entry : nodesToReplace.entrySet()) {
ASTNode nodeToReplace = entry.getKey();
String preferredName = entry.getValue();
if (offset == nodeToReplace.getStartPosition()
&& nodeToReplace.getStartPosition() + nodeToReplace.getLength() <= offset + chars.length) {
if (!(nodeToReplace instanceof Expression)) {
appendTemplateVariableReference(preferredName);
i += nodeToReplace.getLength() - 1;
continue outer;
}
Expression expressionToReplace = (Expression) nodeToReplace;
ITypeBinding typeBinding = expressionToReplace.resolveTypeBinding();
if (typeBinding == null) {
appendTemplateVariableReference(preferredName);
i += nodeToReplace.getLength() - 1;
continue outer;
}
String templateVariableName = createTemplateVariableName(preferredName);
if (!appendTypedTemplateVariableInternal(templateVariableName, "var", typeBinding)) {
appendTemplateVariableReference(templateVariableName);
i += nodeToReplace.getLength() - 1;
continue outer;
}
i += nodeToReplace.getLength() - 1;
continue outer;
}
}
char c = chars[i];
// every non-identifier character can be copied right away. This is necessary since the NodeFinder sometimes
// associates a whitespace with a previous AST node (not exactly understood yet).
if (!Character.isJavaIdentifierPart(c)) {
code.append(c);
continue;
}
NodeFinder nodeFinder = new NodeFinder(enclosingNode, offset, 0);
ASTNode node = nodeFinder.getCoveringNode();
if (
isCoveredBySelection(node)) {
switch (node.getNodeType()) {
case ASTNode.SIMPLE_NAME:
SimpleName name = (SimpleName) node;
IBinding binding = name.resolveBinding();
if (binding == null) {
break;
}
switch (binding.getKind()) {
case IBinding.TYPE:
ITypeBinding typeBinding = (ITypeBinding) binding;
if (isUnqualified(name) && !isDeclaredInSelection(typeBinding)) {
rememberImport(typeBinding);
}
code.append(name);
i += name.getLength() - 1;
continue outer;
case IBinding.METHOD:
IMethodBinding methodBinding = (IMethodBinding) binding;
if (isUnqualifiedMethodInvocation(name) && isStatic(methodBinding)
&& !isDeclaredInSelection(methodBinding)) {
rememberStaticImport(methodBinding);
}
code.append(name);
i += name.getLength() - 1;
continue outer;
case IBinding.VARIABLE:
IVariableBinding variableBinding = (IVariableBinding) binding;
if (isDeclaration(name)) {
if (!appendNewNameTemplateVariable(name.getIdentifier(), variableBinding)) {
code.append(name);
}
} else if (isDeclaredInSelection(variableBinding)) {
appendTemplateVariableReference(variableBinding);
} else if (!isUnqualified(name)) {
code.append(name);
} else if (variableBinding.isField()) {
if (isStatic(variableBinding)) {
code.append(name);
rememberStaticImport(variableBinding);
} else {
if (!appendFieldTemplateVariable(name.getIdentifier(), variableBinding)) {
code.append(name);
}
}
} else {
appendVarTemplateVariable(name.getIdentifier(), variableBinding);
}
i += name.getLength() - 1;
continue outer;
}
}
}
code.append(c);
if (c == '$') {
code.append(c);
}
}
code.append('\n');
replaceLeadingWhitespaces();
appendImportTemplateVariable();
appendImportStaticTemplateVariable();
appendCursorTemplateVariable();
return code.toString();
}
public boolean isCoveredBySelection(ASTNode node) {
int nodeStart = node.getStartPosition();
int nodeEnd = nodeStart + node.getLength();
return textSelection.getOffset() <= nodeStart
&& nodeEnd <= textSelection.getOffset() + textSelection.getLength();
}
private boolean isDeclaredInSelection(IBinding binding) {
ASTNode declaringNode = ast.findDeclaringNode(binding);
if (declaringNode == null) {
return false; // Declared in different compilation unit
}
return isCoveredBySelection(declaringNode);
}
private boolean isUnqualified(SimpleName name) {
return !QualifiedName.NAME_PROPERTY.equals(name.getLocationInParent());
}
private boolean isUnqualifiedMethodInvocation(SimpleName name) {
if (!MethodInvocation.NAME_PROPERTY.equals(name.getLocationInParent())) {
return false;
}
MethodInvocation methodInvocation = (MethodInvocation) name.getParent();
if (methodInvocation.getExpression() != null) {
return false;
}
return true;
}
private boolean isStatic(IBinding binding) {
return Modifier.isStatic(binding.getModifiers());
}
private boolean isDeclaration(SimpleName name) {
if (VariableDeclarationFragment.NAME_PROPERTY.equals(name.getLocationInParent())) {
return true;
} else if (SingleVariableDeclaration.NAME_PROPERTY.equals(name.getLocationInParent())) {
return true;
} else {
return false;
}
}
private void rememberImport(ITypeBinding binding) {
// Remember importable types only. Get the component type if it's an array type
if (binding.isArray()) {
rememberImport(binding.getComponentType());
return;
}
IPackageBinding packageBinding = binding.getPackage();
if (packageBinding == null) {
return; // Either a primitive or some generics-related binding (e.g., a type variable)
}
if (packageBinding.isUnnamed()) {
return;
}
if (packageBinding.getName().equals("java.lang")) { //$NON-NLS-1$
return;
}
ITypeBinding erasure = binding.getErasure();
if (erasure.isRecovered()) {
return;
}
imports.add(erasure.getQualifiedName());
}
private void rememberStaticImport(IMethodBinding method) {
Preconditions.checkArgument(isStatic(method));
rememberStaticImport(method.getDeclaringClass(), method.getName());
}
private void rememberStaticImport(IVariableBinding field) {
Preconditions.checkArgument(field.isField());
Preconditions.checkArgument(isStatic(field));
rememberStaticImport(field.getDeclaringClass(), field.getName());
}
private void rememberStaticImport(@Nullable ITypeBinding declaringTypeBinding, String member) {
if (declaringTypeBinding == null) {
return;
}
IPackageBinding packageBinding = declaringTypeBinding.getPackage();
if (packageBinding == null) {
return; // Either a primitive or some generics-related binding (e.g., a type variable)
}
if (packageBinding.isUnnamed()) {
return;
}
importStatics.add(declaringTypeBinding.getErasure().getQualifiedName() + '.' + member);
}
private boolean appendTemplateVariableReference(String preferredName) {
String templateVariableName = createTemplateVariableName(preferredName);
code.append('$').append('{').append(templateVariableName).append('}');
return true;
}
private boolean appendTemplateVariableReference(IVariableBinding variableBinding) {
String templateVariableName = findTemplateVariableName(variableBinding).orNull();
if (templateVariableName != null) {
code.append('$').append('{').append(templateVariableName).append('}');
return true;
} else {
return false;
}
}
private boolean appendNewNameTemplateVariable(String preferredName, IVariableBinding variableBinding) {
if (appendTemplateVariableReference(variableBinding)) {
return true;
}
String templateVariableName = createTemplateVariableName(preferredName, variableBinding);
ITypeBinding type = variableBinding.getType();
return appendTypedTemplateVariableInternal(templateVariableName, "newName", type); //$NON-NLS-1$
}
private boolean appendFieldTemplateVariable(String preferredName, IVariableBinding variableBinding) {
Preconditions.checkArgument(variableBinding.isField());
if (appendTemplateVariableReference(variableBinding)) {
return true;
}
String templateVariableName = createTemplateVariableName(preferredName, variableBinding);
ITypeBinding typeBinding = variableBinding.getType();
return appendTypedTemplateVariableInternal(templateVariableName, "field", typeBinding); //$NON-NLS-1$
}
private boolean appendVarTemplateVariable(String preferredName, IVariableBinding variableBinding) {
Preconditions.checkArgument(!variableBinding.isField());
if (appendTemplateVariableReference(variableBinding)) {
return true;
}
String templateVariableName = createTemplateVariableName(preferredName, variableBinding);
ITypeBinding typeBinding = variableBinding.getType();
return appendTypedTemplateVariableInternal(templateVariableName, "var", typeBinding); //$NON-NLS-1$
}
private boolean appendTypedTemplateVariableInternal(String templateVariableName, String kind,
@Nullable ITypeBinding typeBinding) {
if (typeBinding == null) {
return false;
}
ITypeBinding erasure = typeBinding.getErasure();
if (erasure == null) {
return false;
}
if (erasure.isRecovered()) {
return false;
}
code.append('$').append('{').append(templateVariableName).append(':').append(kind).append('(');
if (typeBinding.isArray()) {
code.append('\'').append(erasure.getQualifiedName()).append('\'');
} else {
code.append(erasure.getQualifiedName());
}
code.append(')').append('}');
return true;
}
private Optional<String> findTemplateVariableName(IVariableBinding variable) {
return Optional.fromNullable(templateVariableNameReferences.get(variable));
}
private String createTemplateVariableName(String preferredName, IVariableBinding variableBinding) {
Preconditions.checkState(!templateVariableNameReferences.containsKey(variableBinding));
String assignedName = createTemplateVariableName(preferredName);
templateVariableNameReferences.put(variableBinding, assignedName);
return assignedName;
}
private String createTemplateVariableName(String preferredName) {
String sanitizedPreferredName = preferredName.replace('$', '_');
String candidateName = sanitizedPreferredName;
Integer i;
if (lastTemplateVariableIndex.containsKey(candidateName)) {
i = lastTemplateVariableIndex.get(candidateName);
} else {
i = 1;
}
while (assignedTemplateVariablesNames.contains(candidateName)) {
candidateName = sanitizedPreferredName.concat(Integer.toString(i++));
}
String assignedName = candidateName;
assignedTemplateVariablesNames.add(assignedName);
lastTemplateVariableIndex.put(assignedName, i);
return assignedName;
}
private boolean appendImportTemplateVariable() {
return appendStringTemplateVariableInternal("import", imports); //$NON-NLS-1$
}
private boolean appendImportStaticTemplateVariable() {
return appendStringTemplateVariableInternal("importStatic", importStatics); //$NON-NLS-1$
}
private boolean appendStringTemplateVariableInternal(String kind, Collection<String> imports) {
if (imports.isEmpty()) {
return false;
}
String joinedImports = Joiner.on(", ").join(imports); //$NON-NLS-1$
code.append('$').append('{').append(':').append(kind).append('(').append(joinedImports).append(')').append('}');
return true;
}
private void appendCursorTemplateVariable() {
code.append("${cursor}").append(LINE_SEPARATOR); //$NON-NLS-1$
}
private void replaceLeadingWhitespaces() {
try {
// fetch the selection's starting line from the editor document to
// determine the number of leading
// whitespace characters to remove from the snippet:
IRegion firstLineInfo = document.getLineInformationOfOffset(textSelection.getOffset());
String line = document.get(firstLineInfo.getOffset(), firstLineInfo.getLength());
int index = 0;
for (; index < line.length(); index++) {
if (!Character.isWhitespace(line.charAt(index))) {
break;
}
}
String wsPrefix = line.substring(0, index);
// rewrite the buffer and try to remove the leading whitespace. This
// is a simple heuristic only...
String[] lines = code.toString().split("\\r?\\n"); //$NON-NLS-1$
code.setLength(0);
for (String l : lines) {
String clean = StringUtils.removeStart(l, wsPrefix);
code.append(clean).append(LINE_SEPARATOR);
}
} catch (BadLocationException e) {
log(ERROR_SNIPPET_REPLACE_LEADING_WHITESPACE_FAILED, e);
}
}
}