org.eclipse.recommenders.jdt.templates.SnippetCodeBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.recommenders.jdt.templates.SnippetCodeBuilder.java

Source

/**
 * 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');

        appendImportTemplateVariable();

        appendImportStaticTemplateVariable();

        appendCursorTemplateVariable();

        replaceLeadingWhitespaces();

        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}"); //$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);
        }
    }
}