org.eclipse.wb.internal.core.utils.ast.AstEditor.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.wb.internal.core.utils.ast.AstEditor.java

Source

/*******************************************************************************
 * Copyright (c) 2011 Google, Inc.
 * 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:
 *    Google, Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.wb.internal.core.utils.ast;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.eclipse.wb.internal.core.utils.GenericsUtils;
import org.eclipse.wb.internal.core.utils.StringUtilities;
import org.eclipse.wb.internal.core.utils.ast.binding.BindingContext;
import org.eclipse.wb.internal.core.utils.ast.binding.DesignerMethodBinding;
import org.eclipse.wb.internal.core.utils.ast.binding.DesignerTypeBinding;
import org.eclipse.wb.internal.core.utils.check.Assert;
import org.eclipse.wb.internal.core.utils.exception.DesignerException;
import org.eclipse.wb.internal.core.utils.exception.ICoreExceptionConstants;
import org.eclipse.wb.internal.core.utils.jdt.core.CodeUtils;
import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils;

import org.eclipse.core.resources.IProject;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BlockComment;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.LineComment;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.TextElement;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IRegion;

import org.apache.commons.lang.StringUtils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * {@link AstEditor} is central point for all AST and source editing operations, such as adding new
 * statements, method invocations, methods, etc.
 * 
 * @author scheglov_ke
 * @coverage core.util.ast
 */
public final class AstEditor {
    public static final String DEFAULT_END_OF_LINE = System.getProperty("line.separator", "\n");
    private static final String REMOVED_COMMENT = "ASTEditor.REMOVED_COMMENT";
    private final ICompilationUnit m_modelUnit;
    private final CompilationUnit m_astUnit;
    private String m_oldContent;
    private final Document m_document;

    ////////////////////////////////////////////////////////////////////////////
    //
    // Constructor
    //
    ////////////////////////////////////////////////////////////////////////////
    public AstEditor(ICompilationUnit modelUnit) throws Exception {
        m_modelUnit = modelUnit;
        m_astUnit = CodeUtils.parseCompilationUnit(modelUnit);
        m_oldContent = m_modelUnit.getBuffer().getContents();
        m_document = new Document(m_oldContent);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Access
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return the {@link IJavaProject}.
     */
    public IJavaProject getJavaProject() {
        return m_modelUnit.getJavaProject();
    }

    /**
     * @return the {@link IProject}.
     */
    public IProject getProject() {
        return getJavaProject().getProject();
    }

    /**
     * @return {@link ICompilationUnit} - model unit.
     */
    public ICompilationUnit getModelUnit() {
        return m_modelUnit;
    }

    /**
     * @return {@link CompilationUnit} - ast unit.
     */
    public CompilationUnit getAstUnit() {
        return m_astUnit;
    }

    /**
     * @return <code>true</code> if {@link CompilationUnit} has error problems.
     */
    public boolean hasCompilationErrors() {
        for (IProblem problem : m_astUnit.getProblems()) {
            if (problem.isError()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return the primary {@link TypeDeclaration}.
     */
    public TypeDeclaration getPrimaryType() {
        String unitName = m_modelUnit.getElementName();
        String typeName = StringUtils.removeEnd(unitName, ".java");
        return AstNodeUtils.getTypeByName(m_astUnit, typeName);
    }

    /**
     * @return the {@link IType} of given {@link TypeDeclaration}.
     */
    public IType getModelType(TypeDeclaration typeDeclaration) {
        String name = typeDeclaration.getName().getIdentifier();
        return m_modelUnit.getType(name);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // "Enclosing" utilities
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return the {@link ASTNode} that starts with given source, such that there are no child node
     *         that also covers same position.
     */
    public ASTNode getEnclosingNode(String source) {
        int position = getSource().indexOf(source);
        Assert.isTrue(position != -1, "Can not find %s in %s.", source, getSource());
        return AstNodeUtils.getEnclosingNode(m_astUnit, position);
    }

    /**
     * @return the {@link ASTNode} that covers passed position, such that there are no child node that
     *         also covers same position.
     */
    public ASTNode getEnclosingNode(int position) {
        return AstNodeUtils.getEnclosingNode(m_astUnit, position);
    }

    /**
     * @return the {@link Statement} that encloses given position.
     */
    public Statement getEnclosingStatement(int position) {
        return AstNodeUtils.getEnclosingStatement(getEnclosingNode(position));
    }

    /**
     * @return the {@link Block} that encloses given position.
     */
    public Block getEnclosingBlock(int position) {
        return AstNodeUtils.getEnclosingBlock(getEnclosingNode(position));
    }

    /**
     * @return the {@link MethodDeclaration} that encloses given position.
     */
    public MethodDeclaration getEnclosingMethod(int position) {
        return AstNodeUtils.getEnclosingMethod(getEnclosingNode(position));
    }

    /**
     * @return the {@link TypeDeclaration} that encloses given position.
     */
    public TypeDeclaration getEnclosingType(int position) {
        return AstNodeUtils.getEnclosingType(getEnclosingNode(position));
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Commit
    //
    ////////////////////////////////////////////////////////////////////////////
    private IASTEditorCommitListener m_commitListener;

    /**
     * Sets the {@link IASTEditorCommitListener}.
     */
    public void setCommitListener(IASTEditorCommitListener commitListener) {
        m_commitListener = commitListener;
    }

    /**
     * Saves current source code into underlying {@link ICompilationUnit}.
     */
    public void commitChanges() throws Exception {
        // pre-listener
        if (m_commitListener != null) {
            m_commitListener.aboutToCommit();
        }
        // set contents, if changed
        {
            String newContent = m_document.get();
            if (!m_oldContent.equals(newContent)
                    && (m_commitListener == null || m_commitListener.canEditBaseFile())) {
                int[] intervals = StringUtilities.getDifferenceIntervals(m_oldContent, newContent);
                m_modelUnit.getBuffer().replace(intervals[0], intervals[1],
                        newContent.substring(intervals[2], intervals[2] + intervals[3]));
                m_oldContent = newContent;
            }
        }
        // post-listener
        if (m_commitListener != null) {
            m_commitListener.commitDone();
        }
    }

    /**
     * Commits changes into {@link ICompilationUnit} and saves it, if not opened in editor.
     */
    public void saveChanges(boolean forceSave) throws Exception {
        commitChanges();
        // save, if not working copy, i.e. not opened in editor
        if (forceSave || !m_modelUnit.isWorkingCopy()) {
            m_modelUnit.getBuffer().save(null, false);
            m_modelUnit.save(null, false);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Text reading
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return the character from document.
     */
    public char getChar(int position) {
        try {
            return m_document.getChar(position);
        } catch (BadLocationException e) {
            throw ReflectionUtils.propagate(e);
        }
    }

    /**
     * @return the whitespace substring that ends at given <code>end</code> position and starts
     *         somewhere before it.
     * 
     * @param includeEOL
     *          is <code>true</code> if characters '\r' and '\n' also should be skipped.
     */
    public String getWhitespaceToLeft(int end, boolean includeEOL) {
        // find first non-whitespace index
        int start = end;
        while (start != 0) {
            char c = getChar(start - 1);
            // in any case we need whitespace
            if (!Character.isWhitespace(c)) {
                break;
            }
            // if EOL is not enabled, check \r and \n
            if (!includeEOL && (c == '\r' || c == '\n')) {
                break;
            }
            //
            start--;
        }
        // return result
        return getSourceBeginEnd(start, end);
    }

    /**
     * @return the line number that contains given index.
     */
    public int getLineNumber(int index) {
        try {
            return m_document.getLineOfOffset(index);
        } catch (Throwable e) {
            throw ReflectionUtils.propagate(e);
        }
    }

    /**
     * @return the index of first character of line that contains given index.
     */
    public int getLineBegin(int index) {
        try {
            IRegion lineInformation = m_document.getLineInformationOfOffset(index);
            return lineInformation.getOffset();
        } catch (Throwable e) {
            throw ReflectionUtils.propagate(e);
        }
    }

    /**
     * @return the index of the first character of the end-of-line marker for the line containing the
     *         given position, or the length of the source if the position is on the last line and is
     *         therefore not followed by an end-of-line marker.
     */
    public int getLineEnd(int index) {
        try {
            IRegion lineInformation = m_document.getLineInformationOfOffset(index);
            return lineInformation.getOffset() + lineInformation.getLength();
        } catch (Throwable e) {
            throw ReflectionUtils.propagate(e);
        }
    }

    /**
     * @return the index of first non-whitespace character.
     * 
     * @param includeEOL
     *          is <code>true</code> if characters '\r' and '\n' also should be skipped.
     */
    public int skipWhitespaceToLeft(int end, boolean includeEOL) {
        // find first non-whitespace index
        int start = end;
        while (start != 0) {
            char c = getChar(start - 1);
            // in any case we need whitespace
            if (!Character.isWhitespace(c)) {
                break;
            }
            // if EOL is not enabled, check \r and \n
            if (!includeEOL && (c == '\r' || c == '\n')) {
                break;
            }
            //
            start--;
        }
        // return result
        return start;
    }

    /**
     * @return the index of first non-whitespace character or start of first line that is empty or
     *         contains only end of line comments (EOLC).
     */
    public int skipWhitespaceAndPureEOLCToLeft(int end) throws Exception {
        // skip whitespace to the left
        int index = end - getWhitespaceToLeft(end, false).length();
        if (index == 0) {
            return index;
        }
        // skip lines with pure EOLC
        while (true) {
            int line = m_document.getLineOfOffset(index - 1);
            int lineOffset = m_document.getLineOffset(line);
            String lineString = getSource(lineOffset, m_document.getLineLength(line));
            int firstNonWhitespace = StringUtils.indexOfAnyBut(lineString, " \t\r\n");
            // check for empty line
            if (firstNonWhitespace == -1) {
                index = lineOffset;
                continue;
            }
            // check if line starts with EOLC, so contains only it
            if (lineString.substring(firstNonWhitespace).startsWith("//")) {
                index = lineOffset;
                continue;
            }
            // previous line was last empty one
            break;
        }
        //
        return index;
    }

    /**
     * If characters at left are EOL, skip one.
     */
    public int skipSingleEOLToLeft(int index) throws Exception {
        if (m_document.getChar(index - 1) == '\n') {
            index--;
        }
        if (m_document.getChar(index - 1) == '\r') {
            index--;
        }
        return index;
    }

    /**
     * @return single source {@link String} for given lines of source.
     * 
     * @param lines
     *          the lines of source
     * @param indent
     *          the "base" indentation
     * @param singleIndent
     *          the indentation that to replace each leading "\t"
     * @param eol
     *          the EOL string
     */
    private static String getIndentedSource(List<String> lines, String indent, String singleIndent, String eol) {
        StringBuffer buffer = new StringBuffer();
        for (String line : lines) {
            // EOL
            if (buffer.length() != 0) {
                buffer.append(eol);
            }
            // indentation
            buffer.append(indent);
            // line
            if (line.length() != 0) {
                int tabsCount = StringUtils.indexOfAnyBut(line, "\t");
                if (tabsCount != -1) {
                    buffer.append(StringUtils.repeat(singleIndent, tabsCount));
                    buffer.append(line.substring(tabsCount));
                } else {
                    buffer.append(StringUtils.repeat(singleIndent, line.length()));
                }
            }
        }
        return buffer.toString();
    }

    /**
     * @return the first occurrence of given sub-string.
     * 
     * @throws IllegalArgumentException
     *           if no such index can be found.
     */
    public int indexOf(String subString) {
        return indexOf(subString, 0);
    }

    /**
     * @return the first occurrence of given sub-string.
     * 
     * @throws IllegalArgumentException
     *           if no such index can be found.
     */
    public int indexOf(String subString, int startPos) {
        int index = indexOf_noEx(subString, startPos);
        if (index != -1) {
            return index;
        }
        // not found
        throw new IllegalArgumentException("Can not find '" + subString + "' starting from " + startPos);
    }

    /**
     * @return the first occurrence of given sub-string, may be <code>-1</code> if not found.
     */
    private int indexOf_noEx(String subString, int startPos) {
        return m_document.get().indexOf(subString, startPos);
    }

    /**
     * @return the first index of any character in the given set of characters.
     * 
     * @throws IllegalArgumentException
     *           if no such index can be found.
     */
    private int indexOfAny(String searchChars, int startPos) throws Exception {
        for (int i = startPos; i < m_document.getLength(); i++) {
            char c = m_document.getChar(i);
            for (int j = 0; j < searchChars.length(); j++) {
                if (searchChars.charAt(j) == c) {
                    return i;
                }
            }
        }
        // not found
        throw new IllegalArgumentException("Can not find '" + searchChars + "' starting from " + startPos);
    }

    /**
     * @return the first index of any character not in the given set of characters.
     * 
     * @throws IllegalArgumentException
     *           if no such index can be found.
     */
    private int indexOfAnyBut(String searchChars, int startPos) throws Exception {
        outer: for (int i = startPos; i < m_document.getLength(); i++) {
            char c = m_document.getChar(i);
            for (int j = 0; j < searchChars.length(); j++) {
                if (searchChars.charAt(j) == c) {
                    continue outer;
                }
            }
            return i;
        }
        // not found
        throw new IllegalArgumentException("Can not find '" + searchChars + "' starting from " + startPos);
    }

    /**
     * @return the nearest index of character when move backward from given position.
     * 
     * @throws IllegalArgumentException
     *           if no such index can be found.
     */
    public int indexOfCharBackward(char searchChar, int endPos) {
        for (int i = endPos - 1; i != 0; i--) {
            char c = getChar(i);
            if (c == searchChar) {
                return i;
            }
        }
        // not found
        throw new IllegalArgumentException("Can not find '" + searchChar + "' starting from " + endPos);
    }

    /**
     * @return the first index of any character not in the given set of characters.
     * 
     * @throws IllegalArgumentException
     *           if no such index can be found.
     */
    private int indexOfAnyButBackward(String searchChars, int startPos) throws Exception {
        outer: for (int i = startPos - 1; i != 0; i--) {
            char c = m_document.getChar(i);
            for (int j = 0; j < searchChars.length(); j++) {
                if (searchChars.charAt(j) == c) {
                    continue outer;
                }
            }
            return i;
        }
        // not found
        throw new IllegalArgumentException("Can not find '" + searchChars + "' starting from " + startPos);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // EOL comments
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return the index of passed {@link StringLiteral} on same line.
     */
    public int getStringLiteralNumberOnLine(StringLiteral stringLiteral) {
        final int slLine = getLineNumber(stringLiteral.getStartPosition());
        // find shortest node that fills full line
        ASTNode lineNode = stringLiteral;
        while (getLineNumber(lineNode.getStartPosition()) == slLine
                && getLineNumber(lineNode.getStartPosition() + lineNode.getLength()) == slLine) {
            lineNode = lineNode.getParent();
        }
        // find all literals on this line
        final List<StringLiteral> literals = Lists.newArrayList();
        lineNode.accept(new ASTVisitor() {
            @Override
            public void endVisit(StringLiteral literal) {
                if (getLineNumber(literal.getStartPosition()) == slLine) {
                    literals.add(literal);
                }
            }
        });
        // find index of given literal
        return literals.indexOf(stringLiteral);
    }

    /**
     * Insert the given EOL comment at the end of the line containing the given position.
     */
    public void addEndOfLineComment(int position, String comment) throws Exception {
        int endOfLinePosition = getLineEnd(position);
        replaceSubstring(endOfLinePosition, 0, comment);
    }

    /**
     * @return the end of line comment (including leading <code>"//"</code>) at the end of the line
     *         containing the given position.
     */
    public String getEndOfLineComment(int position) {
        int lineBegin = getLineBegin(position);
        int lineEnd = getLineEnd(position);
        int commentBegin = indexOf_noEx("//", lineBegin);
        if (commentBegin != -1 && commentBegin < lineEnd) {
            return getSource(commentBegin, lineEnd - commentBegin);
        }
        return null;
    }

    /**
     * Remove the end of line comment at the end of the line containing the given position.
     */
    public void removeEndOfLineComment(int position, String commentToRemove) throws Exception {
        int lineBegin = getLineBegin(position);
        int lineEnd = getLineEnd(position);
        int commentBegin = indexOf_noEx(commentToRemove, lineBegin);
        if (commentBegin != -1 && commentBegin < lineEnd) {
            int commentEnd = commentBegin + commentToRemove.length();
            // update comment end
            commentEnd = indexOfAnyBut(" \t", commentEnd);
            // replace
            replaceSubstring(commentBegin, commentEnd - commentBegin, "");
            // remove leading whitespace (but keep at least one, if there is comment)
            {
                int newCommentBegin = indexOfAnyButBackward(" \t", commentBegin) + 1;
                if (newCommentBegin != commentBegin) {
                    if (m_document.get(commentBegin, 2).equals("//")) {
                        newCommentBegin++;
                    }
                    replaceSubstring(newCommentBegin, commentBegin - newCommentBegin, "");
                }
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Text editing
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return the internal {@link Document}, should be used only internally.
     */
    Document getBuffer() {
        return m_document;
    }

    /**
     * @return the full source of {@link CompilationUnit}.
     */
    public String getSource() {
        return m_document.get();
    }

    /**
     * Sets the full source of {@link CompilationUnit}. Note that it does not update AST, so
     * practically can be used only to restore some old source as last step of using this
     * {@link AstEditor}.
     */
    public void setSource(String source) {
        m_document.set(source);
    }

    /**
     * @return the source corresponding to the given {@link ASTNode}.
     */
    public String getSource(ASTNode node) {
        return getSource(node.getStartPosition(), node.getLength());
    }

    /**
     * @return the substring of source with given start position and length.
     */
    public String getSource(int start, int length) {
        try {
            return m_document.get(start, length);
        } catch (Throwable e) {
            throw ReflectionUtils.propagate(e);
        }
    }

    /**
     * @return the substring of source with given begin/end positions.
     */
    public String getSourceBeginEnd(int begin, int end) {
        return getSource(begin, end - begin);
    }

    /**
     * Examples:
     * 
     * <pre>
      * SWT.NONE = org.eclipse.swt.SWT.NONE
      * new JButton() = new javax.swing.JButton()
      * </pre>
     * 
     * @param theNode
     *          the {@link ASTNode} to get the source.
     * @param transformer
     *          the {@link Function} that can participate in node to source transformation by
     *          providing alternative source for some nodes. Can be <code>null</code>, in not
     *          additional transformation required. If transformer returns not <code>null</code>, we
     *          use it instead of its original source; if <code>null</code> - we continue with
     *          original source.
     * 
     * @return the source of {@link ASTNode} in "external form", i.e. with fully qualified types.
     */
    @SuppressWarnings("restriction")
    public String getExternalSource(final ASTNode theNode, final Function<ASTNode, String> transformer) {
        final StringBuffer buffer = new StringBuffer(getSource(theNode));
        // remember positions for all nodes
        final Map<ASTNode, Integer> nodePositions = Maps.newHashMap();
        theNode.accept(new ASTVisitor() {
            @Override
            public void postVisit(ASTNode _node) {
                nodePositions.put(_node, _node.getStartPosition());
            }
        });
        // replace "name" with "qualified name"
        theNode.accept(new org.eclipse.jdt.internal.corext.dom.GenericVisitor() {
            @Override
            protected boolean visitNode(ASTNode node) {
                if (transformer != null) {
                    String source = transformer.apply(node);
                    if (source != null) {
                        replace(node, source);
                        return false;
                    }
                }
                return true;
            }

            @Override
            public void endVisit(SimpleName name) {
                if (!AstNodeUtils.isVariable(name)) {
                    StructuralPropertyDescriptor location = name.getLocationInParent();
                    if (location == SimpleType.NAME_PROPERTY || location == QualifiedName.QUALIFIER_PROPERTY
                            || location == ClassInstanceCreation.NAME_PROPERTY
                            || location == MethodInvocation.EXPRESSION_PROPERTY) {
                        String fullyQualifiedName = AstNodeUtils.getFullyQualifiedName(name, false);
                        replace(name, fullyQualifiedName);
                    }
                }
            }

            /**
             * Replace given ASTNode with different source, with updating positions for other nodes.
             */
            private void replace(ASTNode node, String newSource) {
                int nodePosition = nodePositions.get(node);
                // update source
                {
                    int sourceStart = nodePosition - theNode.getStartPosition();
                    int sourceEnd = sourceStart + node.getLength();
                    buffer.replace(sourceStart, sourceEnd, newSource);
                }
                // update positions for nodes
                int lengthDelta = newSource.length() - node.getLength();
                for (Map.Entry<ASTNode, Integer> entry : nodePositions.entrySet()) {
                    Integer position = entry.getValue();
                    if (position > nodePosition) {
                        entry.setValue(position + lengthDelta);
                    }
                }
            }
        });
        // OK, we have updated source
        return buffer.toString();
    }

    /**
     * @return the source of given {@link ITypeBinding}, including generics.
     */
    public String getTypeBindingSource(ITypeBinding typeBinding) {
        String genericTypeName = AstNodeUtils.getFullyQualifiedName(typeBinding, false, true);
        genericTypeName = StringUtils.replace(genericTypeName, ",", ", ");
        return genericTypeName;
    }

    /**
     * @see #replaceSubstring(int, int, String)
     */
    public void replaceSubstring(ASTNode node, String replacement) throws Exception {
        replaceSubstring(node.getStartPosition(), node.getLength(), replacement);
    }

    /**
     * Replaces the substring starting at the given start position with the given length by the given
     * replacement text, modifying the source ranges for the nodes in the AST in the process.
     * 
     * @param oldStart
     *          the index of the first character being replaced
     * @param oldLength
     *          the number of characters being replaced
     * @param replacement
     *          the text with which they are being replaced
     */
    public void replaceSubstring(final int oldStart, int oldLength, String replacement) throws Exception {
        replaceSubstring_markRemovedComments(oldStart, oldLength);
        List<Comment> commentList = getCommentList();
        // replace text
        //System.out.println("|" + m_document.get(oldStart, oldLength) + "| -> |" + replacement + "|");
        m_document.replace(oldStart, oldLength, replacement);
        // prepare positions
        final int newLength = replacement.length();
        final int difference = newLength - oldLength;
        final int oldEnd = oldStart + oldLength;
        // prepare visitor
        ASTVisitor visitor = new ASTVisitor(true) {
            @Override
            public void postVisit(ASTNode node) {
                int position = node.getStartPosition();
                int length = node.getLength();
                int end = position + length;
                // sanity checks
                {
                    // we can not start replacement inside of node, but end outside
                    if (position < oldStart && oldStart < end && oldEnd > end) {
                        throw new DesignerException(ICoreExceptionConstants.AST_EDITOR_REPLACE);
                    }
                    // we can not start replacement outside of node, but end inside
                    if (position < oldEnd && oldEnd < end && oldStart < position) {
                        throw new DesignerException(ICoreExceptionConstants.AST_EDITOR_REPLACE);
                    }
                }
                //
                if (end <= oldStart) {
                    // before changed region: no change
                } else if (position >= oldEnd && !(node instanceof CompilationUnit)) {
                    // after changed region: move
                    node.setSourceRange(position + difference, length);
                } else if (position <= oldStart && position + length >= oldEnd) {
                    // embraces changed region: change length
                    node.setSourceRange(position, length + difference);
                }
                // special handling for AnonymousTypeDeclaration
                {
                    TypeDeclaration anonymous = AnonymousTypeDeclaration.get(node);
                    if (anonymous != null) {
                        anonymous.setSourceRange(node.getStartPosition(), node.getLength());
                    }
                }
            }
        };
        // modify position/length for nodes in AST
        m_astUnit.accept(visitor);
        // update comments
        for (Comment comment : commentList) {
            if (!(comment instanceof Javadoc)) {
                comment.accept(visitor);
            }
        }
    }

    /**
     * When we replace region, we should remove {@link Comment} in it. We can not really remove them
     * (JDT returns unmodifiable list), so we mark them as removed.
     */
    private void replaceSubstring_markRemovedComments(int begin, int length) {
        List<Comment> comments = DomGenerics.getCommentList(m_astUnit);
        for (Iterator<Comment> I = comments.iterator(); I.hasNext();) {
            Comment comment = I.next();
            if (AstNodeUtils.getSourceBegin(comment) >= begin
                    && AstNodeUtils.getSourceEnd(comment) < begin + length) {
                comment.setProperty(REMOVED_COMMENT, Boolean.TRUE);
            }
        }
    }

    /**
     * @return the {@link List} of {@link Comment}'s in this {@link CompilationUnit}.
     */
    public List<Comment> getCommentList() throws Exception {
        List<Comment> comments = Lists.newArrayList();
        comments.addAll(DomGenerics.getCommentList(m_astUnit));
        // clean up
        int documentLength = m_document.getLength();
        for (Iterator<Comment> I = comments.iterator(); I.hasNext();) {
            Comment comment = I.next();
            if (comment.getProperty(REMOVED_COMMENT) != null) {
                I.remove();
                continue;
            }
            if (AstNodeUtils.getSourceBegin(comment) < 0 || AstNodeUtils.getSourceEnd(comment) > documentLength) {
                I.remove();
            } else if (comment instanceof LineComment) {
                if (!getSource(comment).startsWith("//")) {
                    I.remove();
                }
            } else if (comment instanceof BlockComment) {
                if (!getSource(comment).startsWith("/*")) {
                    I.remove();
                }
            }
        }
        // protect from modifications
        return Collections.unmodifiableList(comments);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // ASTNode's replacement
    //
    ////////////////////////////////////////////////////////////////////////////
    private static final Set<Method> m_invalidNodeMethods = Sets.newHashSet();

    /**
     * Replaces given <code>originalNode</code> with <code>replacementNode</code>. This method uses
     * "duck type" implementation, i.e. uses names and types to identify location of nodes instead of
     * direct checks for {@link ASTNode} structures.
     */
    @SuppressWarnings("unchecked")
    public static void replaceNode(ASTNode originalNode, ASTNode replacementNode) throws Exception {
        ASTNode parent = originalNode.getParent();
        // special case: QualifiedName -> FieldAccess
        if (replaceNode_QualifiedName_to_FieldAccess(originalNode, replacementNode)) {
            return;
        }
        // use "duck type"
        Class<?> parentClass = parent.getClass();
        for (Method method : parentClass.getMethods()) {
            if (method.getParameterTypes().length == 0 && !m_invalidNodeMethods.contains(method)) {
                String methodName = method.getName();
                Class<?> returnType = method.getReturnType();
                try {
                    // check for getXXX() and use setXXX()
                    if (methodName.startsWith("get") && returnType.isAssignableFrom(originalNode.getClass())
                            && method.invoke(parent) == originalNode) {
                        String setMethodName = "set" + methodName.substring("get".length());
                        Method setMethod = parentClass.getMethod(setMethodName, new Class[] { returnType });
                        setMethod.invoke(parent, new Object[] { replacementNode });
                        break;
                    }
                    // check for "List xxx()" and use "List.set()"
                    if (returnType == List.class) {
                        List<ASTNode> elements = (List<ASTNode>) method.invoke(parent);
                        int index = elements.indexOf(originalNode);
                        if (index != -1) {
                            elements.set(index, replacementNode);
                            break;
                        }
                    }
                } catch (InvocationTargetException e) {
                    Assert.isTrue(e.getCause() instanceof UnsupportedOperationException);
                    // it is possible that we will try to call method that is not supported in JLS3 AST,
                    // so we should catch exception and ignore such methods
                    m_invalidNodeMethods.add(method);
                }
            }
        }
    }

    /**
     * If we try to replace qualifier of {@link QualifiedName} with generic {@link Expression}, not
     * just {@link Name}, then {@link QualifiedName} itself should be replaced with
     * {@link FieldAccess}.
     */
    private static boolean replaceNode_QualifiedName_to_FieldAccess(ASTNode originalNode, ASTNode replacementNode)
            throws Exception {
        ASTNode parent = originalNode.getParent();
        if (parent instanceof QualifiedName && !(replacementNode instanceof Name)) {
            QualifiedName qualifiedName = (QualifiedName) parent;
            // prepare FieldAccess
            FieldAccess fieldAccess = originalNode.getAST().newFieldAccess();
            AstNodeUtils.copySourceRange(fieldAccess, qualifiedName);
            fieldAccess.setExpression((Expression) replacementNode);
            // use same "name" node
            {
                SimpleName fieldName = qualifiedName.getName();
                qualifiedName.setName(originalNode.getAST().newSimpleName("__wbp_tmp"));
                fieldAccess.setName(fieldName);
            }
            // replace QualifiedName with FieldAccess
            replaceNode(qualifiedName, fieldAccess);
            return true;
        }
        return false;
    }

    /**
     * Replaces given old {@link Expression} with new {@link Expression} corresponding to the given
     * Java source. If source contains EOL, it will be indented.
     * 
     * @return the new {@link Expression}.
     */
    public Expression replaceExpression(Expression oldExpression, String source) throws Exception {
        // check for source with EOL, should be handled as indented
        if (source.indexOf("\n") != -1) {
            String[] lines = StringUtils.split(source, "\n");
            return replaceExpression(oldExpression, Arrays.asList(lines));
        }
        // "normal" source, without EOL
        return replaceExpressionString(oldExpression, source);
    }

    /**
     * Replaces given old {@link Expression} with new {@link Expression} corresponding the Java source
     * given as array of lines.
     * 
     * @return the new {@link Expression}.
     */
    public Expression replaceExpression(Expression oldExpression, List<String> lines) throws Exception {
        // prepare source
        String source;
        {
            // prepare code generation constants
            AstCodeGeneration generation = getGeneration();
            String singleIndent = generation.getIndentation(1);
            String eol = generation.getEndOfLine();
            // prepare enclosing node
            ASTNode enclosingNode = null;
            {
                enclosingNode = AstNodeUtils.getEnclosingStatement(oldExpression);
                if (enclosingNode == null) {
                    enclosingNode = AstNodeUtils.getEnclosingNode(oldExpression, BodyDeclaration.class);
                }
                Assert.isNotNull(enclosingNode, "No enclosing node found for " + oldExpression);
            }
            // indent source
            String indent = getWhitespaceToLeft(enclosingNode.getStartPosition(), false);
            source = getIndentedSource(lines, indent, singleIndent, eol);
            // remove indentation for first line
            source = source.trim();
        }
        //
        return replaceExpressionString(oldExpression, source);
    }

    /**
     * Replaces given old {@link Expression} with new {@link Expression} corresponding to the given
     * Java source. Source used as is, without checks for EOL, indentation, etc.
     * 
     * @return the new {@link Expression}.
     */
    private Expression replaceExpressionString(Expression oldExpression, String source) throws Exception {
        int oldStart = oldExpression.getStartPosition();
        int oldLength = oldExpression.getLength();
        // update source
        source = replaceSourceTemplates(oldStart, source);
        // replace expression
        Expression newExpression = getParser().parseExpression(oldStart, source);
        replaceSubstring(oldStart, oldLength, source);
        replaceNode(oldExpression, newExpression);
        // final step
        resolveImports(newExpression);
        return newExpression;
    }

    /**
     * Replaces single argument of given {@link MethodInvocation}.
     */
    public Expression replaceInvocationArgument(MethodInvocation invocation, int index, String source)
            throws Exception {
        Expression argument = DomGenerics.arguments(invocation).get(index);
        return replaceExpression(argument, source);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Type operations
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Replaces {@link Type} in {@link VariableDeclarationStatement} with new type.<br>
     * We use this method in morphing.
     * 
     * @param declaration
     *          the fragment from {@link VariableDeclarationStatement}. {@link FieldDeclaration}
     *          should have only one fragment.
     * @param newTypeName
     *          the fully qualified name of type to use.
     */
    public void replaceVariableType(VariableDeclaration declaration, String newTypeName) throws Exception {
        Type type;
        VariableDeclarationStatement declarationStatement = null;
        FieldDeclaration fieldDeclaration = null;
        if (declaration.getLocationInParent() == VariableDeclarationStatement.FRAGMENTS_PROPERTY) {
            declarationStatement = (VariableDeclarationStatement) declaration.getParent();
            type = declarationStatement.getType();
        } else if (declaration.getLocationInParent() == FieldDeclaration.FRAGMENTS_PROPERTY) {
            fieldDeclaration = (FieldDeclaration) declaration.getParent();
            type = fieldDeclaration.getType();
        } else {
            throw new IllegalArgumentException("Unknown argument: " + declaration.getClass());
        }
        // do replace
        {
            Type newType = getParser().parseQualifiedType(type.getStartPosition(), newTypeName);
            // replace Type source
            replaceSubstring(type, newTypeName);
            // replace Type node
            if (declarationStatement != null) {
                declarationStatement.setType(newType);
            }
            if (fieldDeclaration != null) {
                fieldDeclaration.setType(newType);
            }
            resolveImports(newType);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Global values
    //
    ////////////////////////////////////////////////////////////////////////////
    private final Map<String, Object> m_globalMap = Maps.newTreeMap();

    /**
     * @return the current global value for given key.
     */
    public Object getGlobalValue(String key) {
        return m_globalMap.get(key);
    }

    /**
     * Sets new global value for given key.
     */
    public void putGlobalValue(String key, Object value) {
        m_globalMap.put(key, value);
    }

    /**
     * Removes global value for given key.
     */
    public void removeGlobalValue(String key) {
        m_globalMap.remove(key);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Unique names generation
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return the unique variable name (for local variable or field).
     * 
     * @param position
     *          the position where new variable will be used, <code>-1</code> if all variables of
     *          {@link CompilationUnit} should be considered
     * @param baseName
     *          the base name for generating
     * @param excludedVariable
     *          the {@link VariableDeclaration} that should be excluded from checking of uniqueness.
     *          We need this for example when we convert local variable to field, in this case using
     *          same unique name of variable as field name is OK. May be <code>null</code>.
     */
    public String getUniqueVariableName(int position, String baseName, VariableDeclaration excludedVariable) {
        // prepare declarations...
        List<VariableDeclaration> declarations = Lists.newArrayList();
        if (position != -1) {
            // ...visible + shadows
            declarations.addAll(AstNodeUtils.getVariableDeclarationsVisibleAt(m_astUnit, position));
            declarations.addAll(AstNodeUtils.getVariableDeclarationsAfter(m_astUnit, position));
        } else {
            // ...all
            declarations.addAll(AstNodeUtils.getVariableDeclarationsAll(m_astUnit));
        }
        // exclude "excluded"
        declarations.remove(excludedVariable);
        // do generation
        return getUniqueVariableName(declarations, baseName);
    }

    /**
     * Generates unique variable name that does not conflict with other variables.
     * 
     * @param declarations
     *          the {@link VariableDeclaration}'s that can conflict with new variable.
     * @param baseName
     *          the base name for generating
     * 
     * @return the unique variable name (for local variable or field).
     */
    public static String getUniqueVariableName(List<VariableDeclaration> declarations, String baseName) {
        // prepare set of conflicting variables identifiers
        final Set<String> existingIdentifiers = Sets.newTreeSet();
        for (VariableDeclaration declaration : declarations) {
            existingIdentifiers.add(declaration.getName().getIdentifier());
        }
        // generate unique name
        return CodeUtils.generateUniqueName(baseName, new Predicate<String>() {
            public boolean apply(String name) {
                return !existingIdentifiers.contains(name);
            }
        });
    }

    /**
     * @return the unique method name.
     */
    public String getUniqueMethodName(String baseName) {
        // prepare set of methods names
        final Set<String> existingMethods = Sets.newTreeSet();
        m_astUnit.accept(new ASTVisitor() {
            @Override
            public void endVisit(MethodDeclaration node) {
                existingMethods.add(node.getName().getIdentifier());
            }

            @Override
            public void endVisit(TypeDeclaration node) {
                addMethodNames(existingMethods, AstNodeUtils.getTypeBinding(node));
            }
        });
        // generate unique name
        return CodeUtils.generateUniqueName(baseName, new Predicate<String>() {
            public boolean apply(String name) {
                return !existingMethods.contains(name);
            }
        });
    }

    /**
     * @return the unique inner type name.
     */
    public String getUniqueTypeName(String baseName) {
        // prepare set of methods names
        final Set<String> existingTypes = Sets.newTreeSet();
        m_astUnit.accept(new ASTVisitor() {
            @Override
            public void endVisit(TypeDeclaration node) {
                existingTypes.add(node.getName().getIdentifier());
            }
        });
        // generate unique name
        return CodeUtils.generateUniqueName(baseName, new Predicate<String>() {
            public boolean apply(String name) {
                return !existingTypes.contains(name);
            }
        });
    }

    /**
     * Adds names for methods declared in given {@link ITypeBinding} and its super-classes.
     */
    private static void addMethodNames(Set<String> existingMethods, ITypeBinding typeBinding) {
        if (typeBinding != null) {
            for (IMethodBinding method : typeBinding.getDeclaredMethods()) {
                existingMethods.add(method.getName());
            }
            addMethodNames(existingMethods, typeBinding.getSuperclass());
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // SimpleName
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Sets new identifier of given {@link SimpleName}.
     */
    public void setIdentifier(SimpleName simpleName, String newIdentifier) throws Exception {
        replaceSubstring(simpleName, newIdentifier);
        simpleName.setIdentifier(newIdentifier);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Parser
    //
    ////////////////////////////////////////////////////////////////////////////
    private final BindingContext m_bindingContext = new BindingContext();
    private final AstParser m_parser = new AstParser(this);

    /**
     * @return the {@link BindingContext} for this {@link AstEditor}.
     */
    public BindingContext getBindingContext() {
        return m_bindingContext;
    }

    /**
     * @return the {@link AstParser} for this {@link AstEditor}.
     */
    public AstParser getParser() {
        return m_parser;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Code generation
    //
    ////////////////////////////////////////////////////////////////////////////
    private final AstCodeGeneration m_generation = new AstCodeGeneration(this);

    /**
     * @return the {@link AstCodeGeneration} for this {@link AstEditor}.
     */
    public AstCodeGeneration getGeneration() {
        return m_generation;
    }

    /**
     * @return the source with replaced "{wbp_XXX}" templates.
     */
    private String replaceSourceTemplates(int position, String src) {
        // replace {wbp_class} with class reference
        if (src.indexOf("{wbp_class}") != -1) {
            String replacement;
            {
                ASTNode coveringNode = getEnclosingNode(position);
                // prepare enclosing method binding
                IMethodBinding methodBinding;
                {
                    MethodDeclaration methodDeclaration = AstNodeUtils.getEnclosingMethod(coveringNode);
                    Assert.isNotNull(methodDeclaration);
                    methodBinding = AstNodeUtils.getMethodBinding(methodDeclaration);
                }
                // prepare replacement
                if (AstNodeUtils.isStatic(methodBinding)) {
                    TypeDeclaration typeDeclaration = AstNodeUtils.getEnclosingType(coveringNode);
                    ITypeBinding typeBinding = typeDeclaration.resolveBinding();
                    replacement = AstNodeUtils.getFullyQualifiedName(typeBinding, false) + ".class";
                } else {
                    replacement = "getClass()";
                }
            }
            // do replace
            src = StringUtils.replace(src, "{wbp_class}", replacement);
        }
        // replace {wbp_classTop} with top-level class reference
        if (src.indexOf("{wbp_classTop}") != -1) {
            String replacement;
            {
                ASTNode coveringNode = getEnclosingNode(position);
                TypeDeclaration typeDeclaration = AstNodeUtils.getEnclosingTypeTop(coveringNode);
                ITypeBinding typeBinding = typeDeclaration.resolveBinding();
                replacement = AstNodeUtils.getFullyQualifiedName(typeBinding, false) + ".class";
            }
            // do replace
            src = StringUtils.replace(src, "{wbp_classTop}", replacement);
        }
        //
        return src;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // ParenthesizedExpression
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Inlines all {@link ParenthesizedExpression} that enclose given {@link Expression}.
     */
    public void inlineParenthesizedExpression(Expression expression) throws Exception {
        while (expression.getParent() instanceof ParenthesizedExpression) {
            ParenthesizedExpression parent = (ParenthesizedExpression) expression.getParent();
            parent.setExpression(expression.getAST().newSimpleName("__wbp_tmp"));
            replaceNode(parent, expression);
            replaceSubstring(parent.getStartPosition(), parent.getLength(), getSource(expression));
            AstNodeUtils.setSourceBegin(expression, parent.getStartPosition());
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // TryStatement
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return <code>true</code> if given {@link Statement} is enclosed into {@link TryStatement} with
     *         {@link CatchClause} which is same or superclass for given.
     */
    public boolean hasEnclosingTryStatement(Statement statement, String requiredException) throws Exception {
        ITypeHierarchy requiredHierarchy;
        {
            IType requiredExceptionType = getJavaProject().findType(requiredException);
            Assert.isNotNull2(requiredExceptionType, "No such exception type: {0}", requiredException);
            requiredHierarchy = requiredExceptionType.newSupertypeHierarchy(null);
        }
        // visit parents
        while (true) {
            ASTNode parent = statement.getParent();
            // TryStatement
            if (parent instanceof TryStatement) {
                TryStatement tryStatement = (TryStatement) parent;
                List<CatchClause> catchClauses = DomGenerics.catchClauses(tryStatement);
                for (CatchClause catchClause : catchClauses) {
                    SingleVariableDeclaration exception = catchClause.getException();
                    String exceptionName = AstNodeUtils.getFullyQualifiedName(exception, false);
                    IType exceptionType = getJavaProject().findType(exceptionName);
                    if (requiredHierarchy.contains(exceptionType)) {
                        return true;
                    }
                }
            }
            // enclosing Statement or stop
            if (parent instanceof Statement) {
                statement = (Statement) parent;
            } else {
                return false;
            }
        }
    }

    /**
     * Encloses given {@link Statement} in {@link TryStatement}.
     * 
     * @return the enclosing {@link TryStatement}.
     */
    public TryStatement encloseInTryStatement(Statement statement, String catchExceptionType) throws Exception {
        TryStatement tryStatement;
        {
            String line_1 = "try {";
            String line_2 = "} catch (" + catchExceptionType + " e) {";
            String line_3 = "}";
            tryStatement = (TryStatement) addStatement(ImmutableList.of(line_1, line_2, line_3),
                    new StatementTarget(statement, true));
        }
        moveStatement(statement, new StatementTarget(tryStatement.getBody(), true));
        return tryStatement;
    }

    /**
     * Removes empty {@link TryStatement}s.
     */
    public void removeEmptyTryStatements() {
        m_astUnit.accept(new AstVisitorEx() {
            @Override
            public void endVisitEx(TryStatement node) throws Exception {
                if (node.getBody().statements().isEmpty()) {
                    removeStatement(node);
                }
            }
        });
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Statement operations
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Encloses given {@link Statement} in {@link Block}.
     * 
     * @return the enclosing {@link Block}.
     */
    public Block encloseInBlock(Statement statement) throws Exception {
        // add new Block
        Block block = (Block) addStatement(ImmutableList.of("{", "}"), new StatementTarget(statement, true));
        // move Statement into Block
        moveStatement(statement, new StatementTarget(block, true));
        // OK, return Block
        return block;
    }

    /**
     * Inlines given {@link Block} into its parent {@link Block}.
     */
    public void inlineBlock(Block block) throws Exception {
        StatementTarget target = new StatementTarget((Statement) block, true);
        // move Statement's
        List<Statement> statements = Lists.newArrayList(DomGenerics.statements(block));
        for (Statement statement : statements) {
            moveStatement(statement, target);
        }
        // remove Block
        removeStatement(block);
    }

    /**
     * Ensures that given {@link Statement} is child of {@link Block}, so we can add new
     * {@link Statement}s relative given one.
     */
    private Block ensureParentBlock(Statement statement) throws Exception {
        ASTNode parent = statement.getParent();
        // already in Block
        if (parent instanceof Block) {
            return (Block) parent;
        }
        // prepare code generation constants
        AstCodeGeneration generation = getGeneration();
        String singleIndent = generation.getIndentation(1);
        String eol = generation.getEndOfLine();
        String indent = getWhitespaceToLeft(parent.getStartPosition(), false);
        // initial state
        boolean statementIsLastInParent = AstNodeUtils.getSourceEnd(parent) == AstNodeUtils.getSourceEnd(statement);
        // prepare new Block
        int positionForBlock;
        int positionForStatement;
        Block newBlock;
        {
            int statementBegin = statement.getStartPosition();
            positionForBlock = indexOfAnyButBackward(" \t\r\n", statementBegin) + 1;
            String source = " {" + eol;
            source += indent + singleIndent;
            positionForStatement = positionForBlock + source.length();
            source += eol;
            source += indent + "}";
            replaceSubstring(positionForBlock, statementBegin - positionForBlock, source);
            // parse
            newBlock = (Block) getParser().parseStatement(positionForBlock, source);
        }
        // move Statement into Block: source
        {
            int statementBegin = statement.getStartPosition();
            int statementEnd = getStatementEndIndex(statement);
            int statementLength = statementEnd - statementBegin;
            moveSource(positionForStatement, statementBegin, statementLength);
            AstNodeUtils.setSourceLength(newBlock, newBlock.getLength() + statementLength);
        }
        // move Statement into Block: node
        replaceNode(statement, newBlock);
        DomGenerics.statements(newBlock).add(statement);
        // cut parent to Block
        if (statementIsLastInParent) {
            AstNodeUtils.setSourceEnd(parent, newBlock);
        }
        // done
        return newBlock;
    }

    /**
     * Adds new {@link Statement} with given source in given {@link StatementTarget}.
     * 
     * @param source
     *          the source for new {@link Statement} (with trailing ';'). It can contains
     *          <code>\n</code>, so can be multi-line and will be indented.
     * @param target
     *          describes location for new {@link Statement}.
     */
    public Statement addStatement(String source, StatementTarget target) throws Exception {
        String[] lines = StringUtils.split(source, '\n');
        return addStatement(Arrays.asList(lines), target);
    }

    /**
     * Adds new {@link Statement} with given source in given {@link StatementTarget}.
     * 
     * @param lines
     *          the lines of source (possible with empty lines) with single statement.
     * @param target
     *          describes location for new {@link Statement}.
     */
    public Statement addStatement(List<String> lines, StatementTarget target) throws Exception {
        Assert.isNotNull(lines);
        // statement or method declaration required
        Assert.isTrue(target.getBlock() != null || target.getStatement() != null);
        // prepare code generation constants
        AstCodeGeneration generation = getGeneration();
        String singleIndent = generation.getIndentation(1);
        String eol = generation.getEndOfLine();
        //
        if (target.getStatement() != null) {
            Statement targetStatement = target.getStatement();
            Block targetBlock = ensureParentBlock(targetStatement);
            // prepare information about target statement
            String indent = getWhitespaceToLeft(targetStatement.getStartPosition(), false);
            int index = targetBlock.statements().indexOf(targetStatement);
            // prepare source
            String source = getIndentedSource(lines, indent, singleIndent, eol);
            source = replaceSourceTemplates(targetStatement.getStartPosition(), source);
            // add statement
            Statement newStatement;
            if (target.isBefore()) {
                // add before any empty lines and EOLC lines
                int position = skipWhitespaceAndPureEOLCToLeft(targetStatement.getStartPosition());
                // add source
                replaceSubstring(position, 0, source + eol);
                // parse statement
                newStatement = getParser().parseStatement(position, source);
                DomGenerics.statements(targetBlock).add(index, newStatement);
            } else {
                // move at the end of target
                int position = getStatementEndIndex(targetStatement);
                source = eol + source;
                // add source
                replaceSubstring(position, 0, source);
                // parse statement
                newStatement = getParser().parseStatement(position, source);
                DomGenerics.statements(targetBlock).add(index + 1, newStatement);
            }
            //
            resolveImports(newStatement);
            return newStatement;
        } else {
            Block targetBlock = target.getBlock();
            // prepare indentation
            String indent;
            {
                if (targetBlock.getParent() instanceof MethodDeclaration) {
                    indent = getWhitespaceToLeft(targetBlock.getParent().getStartPosition(), false);
                } else {
                    indent = getWhitespaceToLeft(targetBlock.getStartPosition(), false);
                }
                indent += singleIndent;
            }
            // prepare source
            String source = getIndentedSource(lines, indent, singleIndent, eol);
            source = replaceSourceTemplates(targetBlock.getStartPosition(), source);
            // add statement
            Statement newStatement;
            if (target.isBefore()) {
                // prepare position as position after '{' of block
                int position;
                {
                    position = targetBlock.getStartPosition();
                    Assert.isTrue(position != -1);
                    position++;
                }
                // add source
                {
                    String prefix = eol;
                    replaceSubstring(position, 0, prefix + source);
                    position += prefix.length();
                }
                // parse statement
                newStatement = getParser().parseStatement(position, source);
                DomGenerics.statements(targetBlock).add(0, newStatement);
            } else {
                // prepare position as position of '}' of block
                int position = AstNodeUtils.getSourceEnd(targetBlock) - 1;
                String endMethodIndent = getWhitespaceToLeft(position, false);
                position -= endMethodIndent.length();
                // add source
                replaceSubstring(position, 0, source + eol);
                // parse statement
                newStatement = getParser().parseStatement(position, source);
                DomGenerics.statements(targetBlock).add(newStatement);
            }
            //
            resolveImports(newStatement);
            return newStatement;
        }
    }

    /**
     * Removes given {@link Statement}. It removes also any whitespace and comments between removing
     * {@link Statement}.
     */
    public void removeStatement(Statement statement) throws Exception {
        if (AstNodeUtils.isDanglingNode(statement)) {
            return;
        }
        Block block = (Block) statement.getParent();
        List<Statement> statements = DomGenerics.statements(block);
        //
        if (statements.size() == 1 && block.getParent() instanceof Block) {
            removeStatement(block);
        } else {
            // prepare start of source to remove
            int startIndex;
            {
                int index = statements.indexOf(statement);
                if (index != 0) {
                    Statement prevStatement = statements.get(index - 1);
                    startIndex = AstNodeUtils.getSourceEnd(prevStatement);
                } else {
                    startIndex = block.getStartPosition() + "{".length();
                }
            }
            // prepare end of source to remove
            int endIndex = getStatementEndIndex(statement);
            // remove statement and corresponding source
            statements.remove(statement);
            replaceSubstring(startIndex, endIndex - startIndex, "");
        }
    }

    /**
     * Removes the {@link Statement} that encloses given {@link ASTNode}.
     */
    public void removeEnclosingStatement(ASTNode node) throws Exception {
        Statement statement = AstNodeUtils.getEnclosingStatement(node);
        removeStatement(statement);
    }

    /**
     * Re-indents source at place.
     */
    private void reindentSource(int sourceStart, int sourceLength, String indent, String eol) throws Exception {
        // prepare lines
        String[] lines;
        {
            String source = getSource(sourceStart, sourceLength);
            lines = StringUtils.splitByWholeSeparatorPreserveAllTokens(source, eol);
        }
        // change indentation for lines
        int position = sourceStart;
        String oldIndent = null;
        for (String line : lines) {
            // prepare line indentation
            String lineIndent = "";
            {
                int endOfIndent = StringUtils.indexOfAnyBut(line, "\t ");
                if (endOfIndent != -1) {
                    lineIndent = line.substring(0, endOfIndent);
                } else {
                    lineIndent = line;
                }
            }
            // use indentation of first line as base
            if (oldIndent == null) {
                oldIndent = lineIndent;
            }
            // replace indentation
            if (lineIndent.startsWith(oldIndent)) {
                replaceSubstring(position, oldIndent.length(), indent);
                position += indent.length() - oldIndent.length();
            }
            // move position to next line
            position += line.length() + eol.length();
        }
    }

    /**
     * Move statement to given {@link StatementTarget}.
     * 
     * @param target
     *          the new position for statement
     * @param statement
     *          the statement to move
     * @param includePrefixComment
     *          flag to mark that comment before statement also should be moved
     */
    public void moveStatement(Statement statement, StatementTarget target) throws Exception {
        // statement or method declaration required
        Assert.isTrue(target.getBlock() != null || target.getStatement() != null);
        // check for no-op
        if (target.getStatement() != null) {
            Statement targetStatement = target.getStatement();
            // adding relative same statement
            if (targetStatement == statement) {
                return;
            }
            // adding before current next statement
            Block targetBlock = (Block) targetStatement.getParent();
            List<Statement> targetStatements = DomGenerics.statements(targetBlock);
            if (statement.getParent() == targetStatement.getParent()) {
                if (target.isBefore()) {
                    // statement before target
                    if (targetStatements.indexOf(statement) == targetStatements.indexOf(targetStatement) - 1) {
                        return;
                    }
                } else {
                    // statement after target
                    if (targetStatements.indexOf(statement) == targetStatements.indexOf(targetStatement) + 1) {
                        return;
                    }
                }
            }
        } else {
            Block targetBlock = target.getBlock();
            List<Statement> targetStatements = DomGenerics.statements(targetBlock);
            if (statement.getParent() == targetBlock) {
                if (target.isBefore()) {
                    if (targetStatements.indexOf(statement) == 0) {
                        return;
                    }
                } else {
                    if (targetStatements.indexOf(statement) == targetStatements.size() - 1) {
                        return;
                    }
                }
            }
        }
        // prepare code generation constants
        AstCodeGeneration generation = getGeneration();
        String singleIndent = generation.getIndentation(1);
        String eol = generation.getEndOfLine();
        // prepare source location
        Block sourceBlock = (Block) statement.getParent();
        int sourceBegin = skipWhitespaceAndPureEOLCToLeft(statement.getStartPosition());
        int sourceLength = getStatementEndIndex(statement) - sourceBegin;
        // remove leading EOL
        {
            int leftEOLEnd = sourceBegin;
            int leftEOLBegin = skipSingleEOLToLeft(leftEOLEnd);
            int leftEOLLength = leftEOLEnd - leftEOLBegin;
            //
            replaceSubstring(leftEOLBegin, leftEOLLength, "");
            sourceBegin -= leftEOLLength;
        }
        //
        if (target.getStatement() != null) {
            Statement targetStatement = target.getStatement();
            String indent = getWhitespaceToLeft(targetStatement.getStartPosition(), false);
            //
            int position;
            if (target.isBefore()) {
                // move before any empty lines and EOLC lines
                position = skipWhitespaceAndPureEOLCToLeft(targetStatement.getStartPosition());
                // move source
                position = moveSource(position, sourceBegin, sourceLength);
                // add EOL after statement
                replaceSubstring(position + sourceLength, 0, eol);
            } else {
                // move at the end of target
                position = getStatementEndIndex(targetStatement);
                // move source
                position = moveSource(position, sourceBegin, sourceLength);
                // add EOL before statement
                replaceSubstring(position, 0, eol);
                position += eol.length();
            }
            // move node
            {
                Block targetBlock = (Block) targetStatement.getParent();
                int index = targetBlock.statements().indexOf(targetStatement);
                if (!target.isBefore()) {
                    index++;
                }
                if (sourceBlock == targetBlock && sourceBlock.statements().indexOf(statement) < index) {
                    index--;
                }
                sourceBlock.statements().remove(statement);
                DomGenerics.statements(targetBlock).add(index, statement);
            }
            // re-indent
            reindentSource(position, sourceLength, indent, eol);
        } else {
            Block targetBlock = target.getBlock();
            // prepare indentation
            String indent;
            {
                ASTNode indentNode = targetBlock;
                if (targetBlock.getLocationInParent() == MethodDeclaration.BODY_PROPERTY
                        || targetBlock.getLocationInParent() == TryStatement.BODY_PROPERTY) {
                    indentNode = targetBlock.getParent();
                }
                indent = getWhitespaceToLeft(indentNode.getStartPosition(), false);
                indent += singleIndent;
            }
            //
            int position;
            if (target.isBefore()) {
                // prepare position as position after '{' of block
                {
                    position = targetBlock.getStartPosition();
                    Assert.isTrue(position != -1);
                    position++;
                }
                // move source
                position = moveSource(position, sourceBegin, sourceLength);
                // add EOL before statement
                replaceSubstring(position, 0, eol);
                position += eol.length();
                // move node
                sourceBlock.statements().remove(statement);
                DomGenerics.statements(targetBlock).add(0, statement);
            } else {
                // prepare position as position of '}' of block
                position = AstNodeUtils.getSourceEnd(targetBlock) - 1;
                String endMethodIndent = getWhitespaceToLeft(position, false);
                position -= endMethodIndent.length();
                // move source
                position = moveSource(position, sourceBegin, sourceLength);
                // add EOL after statement
                replaceSubstring(position + sourceLength, 0, eol);
                // move node
                sourceBlock.statements().remove(statement);
                DomGenerics.statements(targetBlock).add(statement);
            }
            // re-indent
            reindentSource(position, sourceLength, indent, eol);
        }
    }

    /**
     * Moves piece of source with given start/length to target position.
     * 
     * @return the new value of target
     */
    private int moveSource(int target, int start, int length) throws Exception {
        String source = getSource(start, length);
        // prepare locations
        final int b_pos = start;
        final int b_len = length;
        final int b_end = b_pos + b_len;
        final int t_pos = target;
        //
        if (b_pos > t_pos) {
            m_document.replace(b_pos, b_len, "");
            m_document.replace(t_pos, 0, source);
            // modify source ranges for AST nodes
            m_astUnit.accept(new ASTVisitor(true) {
                @Override
                public void postVisit(ASTNode node) {
                    int n_pos = node.getStartPosition();
                    int n_len = node.getLength();
                    int n_end = n_pos + n_len;
                    while (true) {
                        // node starts after source end
                        if (n_pos >= b_end) {
                            return;
                        }
                        // node ends before target
                        if (n_end <= t_pos) {
                            return;
                        }
                        // node contains source and target
                        if (n_pos < t_pos && n_end > b_end) {
                            return;
                        }
                        // node is inside of block
                        if (n_pos >= b_pos && n_end <= b_end) {
                            node.setSourceRange(t_pos + n_pos - b_pos, n_len);
                            return;
                        }
                        // node is between target and source
                        if (n_pos >= t_pos && n_end <= b_pos) {
                            node.setSourceRange(n_pos + b_len, n_len);
                            return;
                        }
                        // node contains target
                        if (n_pos < t_pos && n_end > t_pos) {
                            node.setSourceRange(n_pos, n_len + b_len);
                            return;
                        }
                        // node contains source
                        /*if (n_pos < b_pos && n_end > b_end)*/ {
                            node.setSourceRange(n_pos + b_len, n_len - b_len);
                            return;
                        }
                    }
                }
            });
        } else {
            m_document.replace(t_pos, 0, source);
            m_document.replace(b_pos, b_len, "");
            // modify source ranges for AST nodes
            m_astUnit.accept(new ASTVisitor(true) {
                @Override
                public void postVisit(ASTNode node) {
                    int n_pos = node.getStartPosition();
                    int n_len = node.getLength();
                    int n_end = n_pos + n_len;
                    // node ends before source
                    if (n_end <= b_pos) {
                        return;
                    }
                    // node starts after target
                    if (n_pos >= t_pos) {
                        return;
                    }
                    // node contains source and target
                    if (n_pos < b_pos && n_end > t_pos) {
                        return;
                    }
                    // node was inside of block
                    if (n_pos >= b_pos && n_end <= b_end) {
                        node.setSourceRange(t_pos - b_len + n_pos - b_pos, n_len);
                        return;
                    }
                    // node is between source and target
                    if (n_pos >= b_end && n_end <= t_pos) {
                        node.setSourceRange(n_pos - b_len, n_len);
                        return;
                    }
                    // node contains target
                    if (n_pos < t_pos && n_end > t_pos) {
                        node.setSourceRange(n_pos - b_len, n_len + b_len);
                        return;
                    }
                    // node contains source
                    /*if (n_pos < b_pos && n_end > b_end)*/ {
                        node.setSourceRange(n_pos, n_len - b_len);
                        return;
                    }
                }
            });
        }
        // update target
        if (start < target) {
            target -= length;
        }
        return target;
    }

    /**
     * @return the index of character directly after end of given {@link Statement}. This can be
     *         beginning of the next statement or EOL. This method skips any whitespace characters and
     *         end-of-line comments.
     */
    public int getStatementEndIndex(Statement statement) {
        int index = statement.getStartPosition() + statement.getLength();
        char c;
        // skip spaces
        while (true) {
            c = getChar(index++);
            if (Character.isWhitespace(c)) {
                if (c == '\r' || c == '\n') {
                    break;
                }
            } else {
                break;
            }
        }
        // skip end-of-line comment
        if (c == '/' && getChar(index) == '/') {
            while (true) {
                c = getChar(index++);
                if (c == '\r' || c == '\n') {
                    break;
                }
            }
        }
        // return result
        return index - 1;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Fields operations
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Adds new {@link FieldDeclaration} with given source.
     */
    public FieldDeclaration addFieldDeclaration(String source, BodyDeclarationTarget target) throws Exception {
        return addFieldDeclaration(ImmutableList.of(source), target);
    }

    /**
     * Adds new {@link FieldDeclaration} with given source lines.
     */
    public FieldDeclaration addFieldDeclaration(List<String> lines, BodyDeclarationTarget target) throws Exception {
        return (FieldDeclaration) addBodyDeclaration(lines, target);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Methods operations
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Adds new {@link MethodDeclaration} with given source in location of given
     * {@link StatementTarget}.
     * 
     * @param header
     *          the header of method without '{'
     * @param bodyLines
     *          the lines of method body
     * @param target
     *          describes location for new {@link BodyDeclaration}
     */
    public MethodDeclaration addMethodDeclaration(String header, List<String> bodyLines,
            BodyDeclarationTarget target) throws Exception {
        return addMethodDeclaration(ImmutableList.<String>of(), header, bodyLines, target);
    }

    /**
     * Adds new {@link MethodDeclaration} with given source in location of given
     * {@link StatementTarget}.
     * 
     * @param bodyLines
     *          the lines of method JavaDoc and/or annotations
     * @param header
     *          the header of method without '{'
     * @param bodyLines
     *          the lines of method body
     * @param target
     *          describes location for new {@link BodyDeclaration}
     */
    public MethodDeclaration addMethodDeclaration(List<String> annotations, String header, List<String> bodyLines,
            BodyDeclarationTarget target) throws Exception {
        // prepare full method lines
        List<String> lines = Lists.newArrayList();
        {
            lines.addAll(annotations);
            if (bodyLines != null) {
                lines.add(header + " {");
                // body
                String singleIndent = getGeneration().getIndentation(1);
                for (String bodyLine : bodyLines) {
                    lines.add(singleIndent + bodyLine);
                }
                // close method
                lines.add("}");
            } else {
                lines.add(header);
            }
        }
        // add method
        return (MethodDeclaration) addBodyDeclaration(lines, target);
    }

    /**
     * Adds new {@link MethodDeclaration} without body in location of given {@link StatementTarget}.
     * 
     * @param header
     *          the header of method.
     * @param target
     *          describes location for new {@link BodyDeclaration}
     */
    public MethodDeclaration addInterfaceMethodDeclaration(String header, BodyDeclarationTarget target)
            throws Exception {
        List<String> lines = ImmutableList.of(header + ";");
        return (MethodDeclaration) addBodyDeclaration(lines, target);
    }

    /**
     * @return the source for parameters of given {@link MethodDeclaration}.<br>
     * 
     *         <code>public void split(String s, int count)</code> -> <code>String s, int count</code>
     */
    public String getParametersSource(MethodDeclaration method) {
        StringBuffer sb = new StringBuffer();
        for (SingleVariableDeclaration parameter : DomGenerics.parameters(method)) {
            if (sb.length() != 0) {
                sb.append(", ");
            }
            sb.append(getSource(parameter));
        }
        return sb.toString();
    }

    /**
     * @return the array of parameter names of given {@link MethodDeclaration}.
     * 
     *         <code>public void split(String s, int count)</code> -> <code>{"s", "count"}</code>
     */
    public String[] getParameterNames(MethodDeclaration method) {
        List<SingleVariableDeclaration> parameters = DomGenerics.parameters(method);
        String names[] = new String[parameters.size()];
        //
        for (int i = 0; i < names.length; i++) {
            SingleVariableDeclaration parameter = parameters.get(i);
            names[i] = parameter.getName().getIdentifier();
        }
        //
        return names;
    }

    /**
     * Replaces the name of {@link MethodDeclaration}.
     */
    public void replaceMethodName(MethodDeclaration method, String newName) throws Exception {
        setIdentifier(method.getName(), newName);
        replaceMethodBinding(method);
    }

    /**
     * Replaces {@link IMethodBinding} for given {@link MethodDeclaration} according to actual source.
     */
    private void replaceMethodBinding(MethodDeclaration method) throws Exception {
        MethodDeclaration parsedMethod = parseExistingMethod(method);
        replaceMethodBinding(method, parsedMethod);
    }

    /**
     * When we parse existing {@link MethodDeclaration} we should ensure that it is not added into
     * parsing context, or this will cause duplicate method compilation problem
     */
    private MethodDeclaration parseExistingMethod(MethodDeclaration method) throws Exception {
        method.setProperty(AstParser.KEY_IGNORE_THIS_METHOD, Boolean.TRUE);
        try {
            return (MethodDeclaration) m_parser.parseBodyDeclaration(method.getStartPosition(), "");
        } finally {
            method.setProperty(AstParser.KEY_IGNORE_THIS_METHOD, null);
        }
    }

    /**
     * Replaces the return {@link Type} in {@link MethodDeclaration} with new type.
     * 
     * @param method
     *          the {@link MethodDeclaration} to replace return type.
     * @param newTypeName
     *          the fully qualified name of type to use.
     */
    public void replaceMethodType(MethodDeclaration method, String newTypeName) throws Exception {
        // replace source/AST
        {
            Type oldType = method.getReturnType2();
            Type newType = getParser().parseQualifiedType(oldType.getStartPosition(), newTypeName);
            // replace Type source
            replaceSubstring(oldType, newTypeName);
            // replace Type node
            {
                method.setReturnType2(newType);
                resolveImports(newType);
            }
        }
        // replace binding
        replaceMethodBinding(method);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Source utils
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return the source of stub copy of given {@link MethodDeclaration}.
     */
    public String getMethodStubSource(MethodDeclaration methodDeclaration) throws Exception {
        IMethodBinding methodBinding = AstNodeUtils.getMethodBinding(methodDeclaration);
        // header
        StringBuilder sb = new StringBuilder();
        sb.append("\t");
        sb.append(getMethodHeaderSource(methodDeclaration));
        sb.append(" {\n");
        // return stub
        {
            ITypeBinding returnType = methodBinding.getReturnType();
            String returnTypeName = AstNodeUtils.getFullyQualifiedName(returnType, false);
            if (!returnTypeName.equals("void")) {
                sb.append("\t\treturn ");
                sb.append(AstParser.getDefaultValue(returnTypeName));
                sb.append(";\n");
            }
        }
        // close body
        sb.append("\t}");
        return sb.toString();
    }

    /**
     * @return the copy of {@link MethodDeclaration} header source, which uses same names for
     *         parameters and fully qualified names for all types.
     */
    public String getMethodHeaderSource(MethodDeclaration method) {
        StringBuffer sb = new StringBuffer();
        IMethodBinding methodBinding = AstNodeUtils.getMethodBinding(method);
        // append modifiers
        {
            List<ASTNode> modifiersNodes = DomGenerics.modifiersNodes(method);
            List<Modifier> modifiers = GenericsUtils.select(modifiersNodes, Modifier.class);
            for (Modifier modifier : modifiers) {
                sb.append(modifier);
                sb.append(" ");
            }
        }
        // append return type
        {
            ITypeBinding returnType = methodBinding.getReturnType();
            String returnTypeName = AstNodeUtils.getFullyQualifiedName(returnType, false);
            sb.append(returnTypeName);
            sb.append(" ");
        }
        // append name
        sb.append(method.getName().getIdentifier());
        sb.append("(");
        // append parameters
        {
            ITypeBinding[] parameterTypes = methodBinding.getParameterTypes();
            List<SingleVariableDeclaration> parameters = DomGenerics.parameters(method);
            for (int i = 0; i < parameterTypes.length; i++) {
                ITypeBinding parameterType = parameterTypes[i];
                String parameterTypeName = AstNodeUtils.getFullyQualifiedName(parameterType, false);
                String parameterName = parameters.get(i).getName().getIdentifier();
                if (i != 0) {
                    sb.append(", ");
                }
                sb.append(parameterTypeName);
                sb.append(" ");
                sb.append(parameterName);
            }
        }
        // done
        sb.append(")");
        return sb.toString();
    }

    /**
     * @return the source for list of type arguments.
     */
    public String getTypeArgumentsSource(ITypeBinding[] typeArguments) {
        if (typeArguments.length == 0) {
            return "";
        }
        // prepare type arguments source
        StringBuilder typeArgumentsBuilder = new StringBuilder();
        typeArgumentsBuilder.append("<");
        for (ITypeBinding typeArgument : typeArguments) {
            if (typeArgumentsBuilder.length() > 1) {
                typeArgumentsBuilder.append(", ");
            }
            typeArgumentsBuilder.append(AstNodeUtils.getFullyQualifiedName(typeArgument, false));
        }
        typeArgumentsBuilder.append(">");
        return typeArgumentsBuilder.toString();
    }

    /**
     * @return the source for type arguments of {@link ClassInstanceCreation}, including support for
     *         possible {@link AnonymousClassDeclaration}
     */
    public String getTypeArgumentsSource(ClassInstanceCreation creation) {
        // prepare ITypeBinding with type arguments
        ITypeBinding typeBinding;
        AnonymousClassDeclaration anonymousDeclaration = creation.getAnonymousClassDeclaration();
        if (anonymousDeclaration != null) {
            typeBinding = AstNodeUtils.getTypeBinding(anonymousDeclaration).getSuperclass();
        } else {
            typeBinding = AstNodeUtils.getTypeBinding(creation);
        }
        // prepare type arguments
        ITypeBinding[] typeArguments = typeBinding.getTypeArguments();
        return getTypeArgumentsSource(typeArguments);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Classes operations
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Adds new {@link TypeDeclaration} with given source in location of given {@link StatementTarget}
     * .
     * 
     * @param lines
     *          the lines of class
     * @param target
     *          describes location for new {@link BodyDeclaration}
     */
    public TypeDeclaration addTypeDeclaration(List<String> lines, BodyDeclarationTarget target) throws Exception {
        return (TypeDeclaration) addBodyDeclaration(lines, target);
    }

    /**
     * Ensures that given {@link TypeDeclaration} interface with given name.
     * 
     * @return <code>false</code> if class already implements given interface and <code>true</code> in
     *         other case.
     */
    public boolean ensureInterfaceImplementation(TypeDeclaration type, String interfaceClassName) throws Exception {
        // find last implement interface and check, may be required interface is already implemented
        Type lastInterface = null;
        for (Type interfaceType : DomGenerics.superInterfaces(type)) {
            String implementedInterface = AstNodeUtils.getFullyQualifiedName(interfaceType, true);
            if (implementedInterface.equals(interfaceClassName)) {
                return false;
            }
            lastInterface = interfaceType;
        }
        // prepare position
        int pos;
        String codePrefix;
        if (lastInterface == null) {
            ASTNode typeName = type.getName();
            if (type.getSuperclassType() != null) {
                typeName = type.getSuperclassType();
            }
            pos = typeName.getStartPosition() + typeName.getLength();
            codePrefix = " implements ";
        } else {
            pos = lastInterface.getStartPosition() + lastInterface.getLength();
            codePrefix = ", ";
        }
        // prepare new interfaceType node
        SimpleType interfaceType;
        {
            TypeLiteral typeLiteral = (TypeLiteral) m_parser.parseExpression(type.getStartPosition(),
                    interfaceClassName + ".class");
            AstNodeUtils.moveNode(typeLiteral, pos + codePrefix.length());
            interfaceType = (SimpleType) typeLiteral.getType();
            typeLiteral.setType(typeLiteral.getAST().newPrimitiveType(PrimitiveType.BOOLEAN));
        }
        // update source/AST
        replaceSubstring(pos, 0, codePrefix + interfaceClassName);
        DomGenerics.superInterfaces(type).add(interfaceType);
        // update ITypeBinding
        {
            ITypeBinding interfaceBinding = AstNodeUtils.getTypeBinding(interfaceType);
            DesignerTypeBinding typeBinding = getDesignerTypeBinding(type);
            typeBinding.addInterface(interfaceBinding);
        }
        // finalize
        resolveImports(interfaceType);
        return true;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // BodyDeclaration operations
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Ensures that {@link MethodDeclaration} declares as thrown exception with given name, or its
     * superclass.
     */
    public void ensureThrownException(MethodDeclaration method, String requiredException) throws Exception {
        ITypeHierarchy requiredHierarchy;
        {
            IType requiredExceptionType = getJavaProject().findType(requiredException);
            Assert.isNotNull2(requiredExceptionType, "No such exception type: {0}", requiredException);
            requiredHierarchy = requiredExceptionType.newSupertypeHierarchy(null);
        }
        // find last exception and check, may be required exception is already declared
        Name lastName = null;
        for (Name declaredTypeName : DomGenerics.thrownExceptions(method)) {
            String declaredName = AstNodeUtils.getFullyQualifiedName(declaredTypeName, false);
            IType declaredType = getJavaProject().findType(declaredName);
            if (requiredHierarchy.contains(declaredType)) {
                return;
            }
            lastName = declaredTypeName;
        }
        // prepare position
        int pos;
        String codePrefix;
        if (lastName == null) {
            ASTNode name = method.getName();
            pos = AstNodeUtils.getSourceEnd(name);
            pos = indexOf(")", pos) + 1;
            codePrefix = " throws ";
        } else {
            pos = AstNodeUtils.getSourceEnd(lastName);
            codePrefix = ", ";
        }
        // prepare new exception nodes
        SimpleType newExceptionType;
        Name newExceptionTypeName;
        {
            TypeLiteral typeLiteral = (TypeLiteral) m_parser.parseExpression(pos, requiredException + ".class");
            AstNodeUtils.moveNode(typeLiteral, pos + codePrefix.length());
            newExceptionType = (SimpleType) typeLiteral.getType();
            newExceptionTypeName = newExceptionType.getName();
            newExceptionType.setName(typeLiteral.getAST().newSimpleName("filler"));
        }
        // update source/AST
        replaceSubstring(pos, 0, codePrefix + requiredException);
        DomGenerics.thrownExceptions(method).add(newExceptionTypeName);
        // update ITypeBinding
        {
            ITypeBinding newExceptionBinding = AstNodeUtils.getTypeBinding(newExceptionType);
            DesignerMethodBinding methodBinding = getDesignerMethodBinding(method);
            methodBinding.addExceptionType(newExceptionBinding);
        }
        // finalize
        resolveImports(method);
    }

    /**
     * Adds new {@link BodyDeclaration} with given source in location of given {@link StatementTarget}
     * .
     * 
     * @param lines
     *          the lines of source for new {@link BodyDeclaration}.
     * @param target
     *          describes location for new {@link BodyDeclaration}.
     */
    private BodyDeclaration addBodyDeclaration(List<String> lines, BodyDeclarationTarget target) throws Exception {
        Assert.isNotNull(lines);
        Assert.isNotNull(target);
        // type or body declaration required
        TypeDeclaration targetType = target.getType();
        BodyDeclaration targetDecl = target.getDeclaration();
        Assert.isTrue(targetType != null || targetDecl != null);
        // prepare code generation constants
        AstCodeGeneration generation = getGeneration();
        String singleIndent = generation.getIndentation(1);
        String eol = generation.getEndOfLine();
        // relative to BodyDeclaration
        if (targetDecl != null) {
            // prepare information about target body declaration
            targetType = (TypeDeclaration) targetDecl.getParent();
            String indent = getWhitespaceToLeft(targetDecl.getStartPosition(), false);
            int index = targetType.bodyDeclarations().indexOf(targetDecl);
            // add new declaration
            BodyDeclaration newDeclaration;
            String source = getIndentedSource(lines, indent, singleIndent, eol);
            if (target.isBefore()) {
                // add before any empty lines and EOLC lines
                int position = skipWhitespaceAndPureEOLCToLeft(targetDecl.getStartPosition());
                source = source + eol;
                // parse declaration
                newDeclaration = getParser().parseBodyDeclaration(position, source);
                // add source
                replaceSubstring(position, 0, source);
                // add declaration
                DomGenerics.bodyDeclarations(targetType).add(index, newDeclaration);
            } else {
                // move at the end of target
                int position = AstNodeUtils.getSourceEnd(targetDecl);
                position = skipWhitespaceEOLCToRight(position);
                source = eol + source;
                // parse declaration
                newDeclaration = getParser().parseBodyDeclaration(position, source);
                // add source
                replaceSubstring(position, 0, source);
                // add declaration
                DomGenerics.bodyDeclarations(targetType).add(index + 1, newDeclaration);
            }
            //
            resolveImports(newDeclaration);
            return newDeclaration;
        }
        // relative to TypeDeclaration
        {
            removeDanglingJavadoc();
            // prepare indent
            String indent;
            {
                ASTNode indentNode;
                if (AnonymousTypeDeclaration.is(targetType)) {
                    indentNode = AstNodeUtils.getEnclosingStatement(targetType);
                } else {
                    indentNode = targetType;
                }
                indent = getWhitespaceToLeft(indentNode.getStartPosition(), false) + singleIndent;
            }
            String source = getIndentedSource(lines, indent, singleIndent, eol);
            // add new body declaration
            BodyDeclaration newDeclaration;
            if (target.isBefore()) {
                // prepare position as position after '{' of type declaration
                int position = indexOfAny("{", targetType.getName().getStartPosition()) + 1;
                source = eol + source;
                // parse declaration
                newDeclaration = getParser().parseBodyDeclaration(position, source);
                // add source
                replaceSubstring(position, 0, source);
                // add declaration
                DomGenerics.bodyDeclarations(targetType).add(0, newDeclaration);
            } else {
                // prepare position as position of '}' of type declaration
                int position = AstNodeUtils.getSourceEnd(targetType) - 1;
                position -= getWhitespaceToLeft(position, false).length();
                source = source + eol;
                // parse declaration
                newDeclaration = getParser().parseBodyDeclaration(position, source);
                // add source
                replaceSubstring(position, 0, source);
                // add declaration
                DomGenerics.bodyDeclarations(targetType).add(newDeclaration);
            }
            //
            resolveImports(newDeclaration);
            return newDeclaration;
        }
    }

    /**
     * Removes given {@link BodyDeclaration}.
     */
    public void removeBodyDeclaration(BodyDeclaration declaration) throws Exception {
        List<BodyDeclaration> declarations;
        if (declaration.getParent() instanceof TypeDeclaration) {
            TypeDeclaration typeDeclaration = (TypeDeclaration) declaration.getParent();
            declarations = DomGenerics.bodyDeclarations(typeDeclaration);
        } else {
            declarations = DomGenerics.bodyDeclarations((AnonymousClassDeclaration) declaration.getParent());
        }
        // prepare start of source to remove
        int startIndex;
        {
            int index = declarations.indexOf(declaration);
            if (index != 0) {
                BodyDeclaration prevDeclaration = declarations.get(index - 1);
                startIndex = prevDeclaration.getStartPosition() + prevDeclaration.getLength();
                startIndex = skipWhitespaceEOLCToRight(startIndex);
            } else {
                if (declaration.getParent() instanceof TypeDeclaration) {
                    TypeDeclaration typeDeclaration = (TypeDeclaration) declaration.getParent();
                    startIndex = indexOfAny("{", typeDeclaration.getName().getStartPosition()) + 1;
                } else {
                    startIndex = indexOfAny("{", declaration.getParent().getStartPosition()) + 1;
                }
            }
        }
        // prepare end of source to remove
        int endIndex;
        {
            endIndex = declaration.getStartPosition() + declaration.getLength();
            endIndex = skipWhitespaceEOLCToRight(endIndex);
        }
        // remove declaration and corresponding source
        declarations.remove(declaration);
        replaceSubstring(startIndex, endIndex - startIndex, "");
    }

    /**
     * @return the position of first (to right) non whitespace character (excluding EOL) on same line.
     *         This method also skips EOL comments.
     */
    private int skipWhitespaceEOLCToRight(int position) throws BadLocationException, Exception {
        for (;; position++) {
            char c = m_document.getChar(position);
            // stop on EOL
            if (c == '\r' || c == '\n') {
                break;
            }
            // skip whitespace
            if (Character.isWhitespace(c)) {
                continue;
            }
            // skip EOL comments
            if (c == '/' && m_document.getChar(position + 1) == '/') {
                position = getLineEnd(position);
                break;
            }
            // stop
            break;
        }
        return position;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // VariableDeclaration operations
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Removes {@link VariableDeclaration}.
     */
    public void removeVariableDeclaration(VariableDeclaration declaration) throws Exception {
        ASTNode parent = declaration.getParent();
        if (parent instanceof FieldDeclaration) {
            // field 
            FieldDeclaration fieldDeclaration = (FieldDeclaration) parent;
            List<VariableDeclarationFragment> fragments = DomGenerics.fragments(fieldDeclaration);
            if (fragments.size() == 1) {
                removeBodyDeclaration(fieldDeclaration);
            } else {
                removeVariableDeclaration(fieldDeclaration, fragments, fragments.indexOf(declaration));
            }
        } else if (parent instanceof VariableDeclarationStatement) {
            // local variable
            VariableDeclarationStatement variableDeclaration = (VariableDeclarationStatement) declaration
                    .getParent();
            List<VariableDeclarationFragment> fragments = DomGenerics.fragments(variableDeclaration);
            if (fragments.size() == 1) {
                removeEnclosingStatement(declaration);
            } else {
                removeVariableDeclaration(variableDeclaration, fragments, fragments.indexOf(declaration));
            }
        } else {
            // can't remove other cases
            throw new IllegalArgumentException(
                    "Can not remove VariableDeclaration '" + declaration.toString() + "'");
        }
    }

    /**
     * Removes {@link VariableDeclarationFragment} from its parent.
     */
    private void removeVariableDeclaration(ASTNode parent, List<VariableDeclarationFragment> fragments, int index)
            throws Exception {
        VariableDeclarationFragment declaration = fragments.get(index);
        Assert.isTrue(fragments.size() > 1, "Last variable must be removed with parent body.",
                getSource(declaration), getSource(parent));
        // prepare source interval to remove
        int sourceBegin;
        int sourceEnd;
        {
            sourceBegin = AstNodeUtils.getSourceBegin(declaration);
            sourceEnd = AstNodeUtils.getSourceEnd(declaration);
            if (index == 0) {
                if (fragments.size() > 1) {
                    sourceEnd = indexOfAnyBut(", \t\r\n", sourceEnd);
                }
            } else {
                sourceBegin = indexOfAnyButBackward(", \t\r\n", sourceBegin) + 1;
            }
        }
        // remove node
        fragments.remove(index);
        // remove source
        replaceSubstring(sourceBegin, sourceEnd - sourceBegin, "");
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Javadoc
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Removes dangling {@link Javadoc} sources at the end of {@link TypeDeclaration}s.
     */
    public void removeDanglingJavadoc() {
        m_astUnit.accept(new AstVisitorEx() {
            @Override
            public void endVisitEx(TypeDeclaration node) throws Exception {
                int typeEnd = AstNodeUtils.getSourceEnd(node) - 1;
                int trailingJavadocEnd = skipWhitespaceAndPureEOLCToLeft(typeEnd);
                trailingJavadocEnd = skipWhitespaceToLeft(trailingJavadocEnd, true);
                // prepare begin/end of last JavaDoc at end of type
                String source = getSource().substring(0, typeEnd);
                int javadocBegin = source.lastIndexOf("/**");
                if (javadocBegin != -1) {
                    int javadocEnd = source.indexOf("*/", javadocBegin) + "*/".length();
                    // if end of last JavaDoc is end of TypeDeclaration, i.e. no BodyDeclaration, remove it 
                    if (javadocEnd == trailingJavadocEnd) {
                        javadocBegin = skipWhitespaceToLeft(javadocBegin, false);
                        replaceSubstring(javadocBegin, typeEnd - javadocBegin, "");
                    }
                }
            }
        });
    }

    /**
     * Sets new text for JavaDoc {@link TagElement}, i.e. {@link TextElement}.
     * 
     * @param declaration
     *          the {@link BodyDeclaration} to update {@link Javadoc}.
     * @param tagName
     *          the name of tag, such as <code>"myTag"</code>, with leading <code>"@"</code>.
     * @param tagText
     *          the text to set for tag, with leading space, or <code>null</code> if tag should be
     *          removed.
     * 
     * @return the {@link TagElement} that has single {@link TextElement} fragment with
     *         <code>tagText</code> as text; or <code>null</code> if tag was removed.
     */
    public TagElement setJavadocTagText(BodyDeclaration declaration, String tagName, String tagText)
            throws Exception {
        Assert.isNotNull(tagName);
        Assert.isLegal(tagName.length() != 0, "Empty name of tag.");
        Assert.isLegal(tagName.startsWith("@"), "Tag name should start with '@'.");
        Javadoc javadoc = declaration.getJavadoc();
        // update/add tag
        if (tagText != null) {
            // update existing JavaDoc
            if (javadoc != null) {
                // try to find existing tag
                for (TagElement tagElement : DomGenerics.tags(javadoc)) {
                    if (tagName.equals(tagElement.getTagName())) {
                        setJavadocTagText_replaceFragments(tagElement, tagText);
                        return tagElement;
                    }
                }
                // add new tag
                {
                    List<TagElement> tags = DomGenerics.tags(javadoc);
                    // prepare position for new TagElement
                    int position;
                    {
                        int javadocPosition = javadoc.getStartPosition();
                        String prefix = getWhitespaceToLeft(javadocPosition, false);
                        String endOfLine = getGeneration().getEndOfLine();
                        if (tags.isEmpty()) {
                            position = javadocPosition + "/**".length();
                        } else {
                            position = AstNodeUtils.getSourceEnd(tags.get(tags.size() - 1));
                        }
                        String newCommentLine = endOfLine + prefix + " * ";
                        replaceSubstring(position, 0, newCommentLine);
                        position += newCommentLine.length();
                    }
                    // replace source
                    String tagSource = tagName + tagText;
                    replaceSubstring(position, 0, tagSource);
                    // add TagElement
                    TagElement tagElement = javadoc.getAST().newTagElement();
                    tagElement.setSourceRange(position, tagSource.length());
                    tagElement.setTagName(tagName);
                    tags.add(tagElement);
                    // add TextElement
                    TextElement textElement = javadoc.getAST().newTextElement();
                    textElement.setSourceRange(position + tagName.length(), tagText.length());
                    textElement.setText(tagText);
                    DomGenerics.fragments(tagElement).add(textElement);
                    // new TagElement
                    return tagElement;
                }
            } else {
                javadoc = setJavadoc(declaration, new String[] { tagName + tagText });
                return DomGenerics.tags(javadoc).get(0);
            }
        }
        // remove tag
        if (javadoc != null) {
            for (Iterator<TagElement> I = DomGenerics.tags(javadoc).iterator(); I.hasNext();) {
                TagElement tagElement = I.next();
                if (tagName.equals(tagElement.getTagName())) {
                    int begin = AstNodeUtils.getSourceBegin(tagElement);
                    int end = AstNodeUtils.getSourceEnd(tagElement);
                    end = indexOfAnyBut(" \t\r\n*", end + 1);
                    // check for removing last line
                    if (getChar(end) == '/') {
                        begin = indexOfAnyButBackward("*", begin);
                    }
                    // replace source
                    replaceSubstring(begin, end - begin, "");
                    // remove tag
                    I.remove();
                    // remove JavaDoc is empty
                    if (DomGenerics.tags(javadoc).isEmpty()) {
                        setJavadoc(declaration, null);
                    }
                    // done
                    break;
                }
            }
        }
        return null;
    }

    private void setJavadocTagText_replaceFragments(TagElement tagElement, String tagText) throws Exception {
        List<ASTNode> fragments = DomGenerics.fragments(tagElement);
        // replace source
        int fragmentsPosition;
        if (!fragments.isEmpty()) {
            ASTNode firstFragment = fragments.get(0);
            fragmentsPosition = AstNodeUtils.getSourceBegin(firstFragment);
            int fragmentsLength = AstNodeUtils.getSourceEnd(tagElement) - fragmentsPosition;
            replaceSubstring(fragmentsPosition, fragmentsLength, tagText);
        } else {
            fragmentsPosition = AstNodeUtils.getSourceEnd(tagElement);
            replaceSubstring(fragmentsPosition, 0, tagText);
            AstNodeUtils.setSourceLength(tagElement, tagElement.getLength() + tagText.length());
        }
        // replace fragments
        fragments.clear();
        TextElement textElement = tagElement.getAST().newTextElement();
        textElement.setSourceRange(fragmentsPosition, tagText.length());
        textElement.setText(tagText);
        fragments.add(textElement);
    }

    /**
     * Sets new {@link Javadoc} comment for {@link BodyDeclaration}.
     * 
     * @param declaration
     *          the {@link BodyDeclaration} to adds comment to.
     * @param lines
     *          the lines for {@link Javadoc} comment, may be <code>null</code> if {@link Javadoc}
     *          should be removed.
     * 
     * @return the added {@link Javadoc} object, or <code>null</code> if {@link Javadoc} was removed.
     */
    public Javadoc setJavadoc(BodyDeclaration declaration, String[] lines) throws Exception {
        Javadoc oldJavadoc = declaration.getJavadoc();
        // set new JavaDoc
        if (lines != null) {
            int position = declaration.getStartPosition();
            // prepare code generation constants
            AstCodeGeneration generation = getGeneration();
            String eol = generation.getEndOfLine();
            String indent = getWhitespaceToLeft(declaration.getStartPosition(), false);
            // prepare source for comment
            String comment;
            {
                StringBuilder sb = new StringBuilder();
                sb.append("/**");
                sb.append(eol);
                for (String line : lines) {
                    sb.append(indent);
                    sb.append(" * ");
                    sb.append(line);
                    sb.append(eol);
                }
                sb.append(indent);
                sb.append(" */");
                comment = sb.toString();
            }
            // prepare JavaDoc
            Javadoc javadoc;
            {
                BodyDeclaration tmpMethod = getParser().parseBodyDeclaration(position,
                        comment + " void __wbp_tmpMethod() {}");
                javadoc = tmpMethod.getJavadoc();
                tmpMethod.setJavadoc(null);
            }
            // set JavaDoc
            if (oldJavadoc != null) {
                int oldLength = oldJavadoc.getLength();
                replaceSubstring(position, oldLength, comment);
            } else {
                comment += eol + indent;
                replaceSubstring(position, 0, comment);
                declaration.setSourceRange(position, comment.length() + declaration.getLength());
            }
            declaration.setJavadoc(javadoc);
            return javadoc;
        }
        // remove existing JavaDoc
        if (oldJavadoc != null) {
            int sourceBegin = oldJavadoc.getStartPosition();
            int sourceEnd = sourceBegin + oldJavadoc.getLength();
            sourceEnd = indexOfAnyBut(" \t\r\n", sourceEnd);
            replaceSubstring(sourceBegin, sourceEnd - sourceBegin, "");
            declaration.setJavadoc(null);
        }
        return null;
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Import operations
    //
    ////////////////////////////////////////////////////////////////////////////
    private boolean m_resolveImports = true;

    /**
     * Ensure that the compilation unit includes an import for the given fully-qualified class name,
     * adding one if it does not already exist.
     * 
     * @param className
     *          the fully qualified name of the class that must be imported
     * 
     * @return the string that should be used as reference on class, can be short name (if import was
     *         successful) or fully qualified name (if there is already imported class with same short
     *         name).
     */
    public String ensureClassImport2(final String className) throws Exception {
        final String shortClassName = CodeUtils.getShortClass(className);
        List<ImportDeclaration> imports = DomGenerics.imports(m_astUnit);
        // check, may be we don't need to add import
        {
            String packageName = CodeUtils.getPackage(className);
            // check for java.lang
            if ("java.lang".equals(packageName)) {
                return shortClassName;
            }
            // check for existing import
            for (Iterator<ImportDeclaration> I = imports.iterator(); I.hasNext();) {
                ImportDeclaration currentImport = I.next();
                String importName = currentImport.getName().toString();
                if (importName.equals(className) || currentImport.isOnDemand() && importName.equals(packageName)) {
                    return shortClassName;
                }
            }
            // check for class in same package
            {
                IPackageDeclaration[] packageDeclarations = m_modelUnit.getPackageDeclarations();
                if (packageDeclarations.length != 0
                        && packageDeclarations[0].getElementName().equals(packageName)) {
                    return shortClassName;
                }
            }
            // check for inner class
            {
                final AtomicBoolean hasInner = new AtomicBoolean();
                m_astUnit.accept(new ASTVisitor() {
                    @Override
                    public boolean visit(Block node) {
                        return false;
                    }

                    @Override
                    public void endVisit(TypeDeclaration node) {
                        String qualifiedTypeName = AstNodeUtils.getFullyQualifiedName(node, false);
                        if (qualifiedTypeName.equals(className)) {
                            hasInner.set(true);
                        }
                    }
                });
                if (hasInner.get()) {
                    return shortClassName;
                }
            }
        }
        // check, may be we can not import because of conflict
        {
            boolean hasOnDemand = false;
            // check, may be there is already import for class with same short name
            for (Iterator<ImportDeclaration> I = imports.iterator(); I.hasNext();) {
                ImportDeclaration currentImport = I.next();
                if (currentImport.isOnDemand()) {
                    hasOnDemand = true;
                    break;
                } else {
                    String importName = currentImport.getName().getFullyQualifiedName();
                    if (CodeUtils.getShortClass(importName).equals(shortClassName)) {
                        return className;
                    }
                }
            }
            // there are on demand imports, we should check AST for imports
            if (hasOnDemand) {
                final AtomicBoolean conflict = new AtomicBoolean();
                m_astUnit.accept(new ASTVisitor() {
                    @Override
                    public void endVisit(SimpleType node) {
                        if (!conflict.get()) {
                            String qualifiedTypeName = AstNodeUtils.getFullyQualifiedName(node, false);
                            boolean isQualified = qualifiedTypeName.equals(getSource(node));
                            boolean hasSameShort = CodeUtils.getShortClass(qualifiedTypeName)
                                    .equals(shortClassName);
                            if (!isQualified && hasSameShort) {
                                conflict.set(true);
                            }
                        }
                    }
                });
                // if short class name conflicts, use long one
                if (conflict.get()) {
                    return className;
                }
            }
            // may be has TypeDeclaration with same short name
            {
                final AtomicBoolean hasTypeDeclarationWithSameShort = new AtomicBoolean();
                m_astUnit.accept(new ASTVisitor() {
                    @Override
                    public boolean visit(Block node) {
                        return false;
                    }

                    @Override
                    public void endVisit(TypeDeclaration node) {
                        if (node.getName().getIdentifier().equals(shortClassName)) {
                            hasTypeDeclarationWithSameShort.set(true);
                        }
                    }
                });
                if (hasTypeDeclarationWithSameShort.get()) {
                    return className;
                }
            }
        }
        // OK, we need to add new import
        {
            String eol = getGeneration().getEndOfLine();
            String sourcePrefix = "";
            String sourceSuffix = "";
            // prepare position for new import
            int position;
            if (!imports.isEmpty()) {
                ImportDeclaration lastImport = imports.get(imports.size() - 1);
                position = AstNodeUtils.getSourceEnd(lastImport);
                sourcePrefix = eol;
            } else if (m_astUnit.getPackage() != null) {
                position = AstNodeUtils.getSourceEnd(m_astUnit.getPackage());
                sourcePrefix = eol;
            } else {
                position = 0;
                sourceSuffix = eol;
            }
            // add new import
            {
                // add import source
                {
                    String source = sourcePrefix + "import " + className + ";" + sourceSuffix;
                    replaceSubstring(position, 0, source);
                }
                // add import node
                {
                    int importPosition = position + sourcePrefix.length();
                    ImportDeclaration newImport = getParser().parseImportDeclaration(importPosition, className);
                    imports.add(newImport);
                }
            }
            // import successful, return short name
            return shortClassName;
        }
    }

    /**
     * Sets the flag if imports should be automatically resolved.
     */
    public void setResolveImports(boolean resolveImports) {
        m_resolveImports = resolveImports;
    }

    /**
     * Given the {@link ASTNode} that can use fully qualified types, this method tries to import
     * qualified types and update node/source accordingly. It is expected that node already added to
     * the AST and source already modified.
     */
    public void resolveImports(final ASTNode node) {
        if (!m_resolveImports) {
            return;
        }
        node.accept(new AstVisitorEx() {
            @Override
            public boolean visitEx(QualifiedName qualifiedName) throws Exception {
                String qualifiedClassName = qualifiedName.getFullyQualifiedName();
                // if we can find IType with this name, consider it as type name
                if (getJavaProject().findType(qualifiedClassName) != null) {
                    String shortClassName = ensureClassImport2(qualifiedClassName);
                    if (!shortClassName.equals(qualifiedClassName)) {
                        int sourceBegin = AstNodeUtils.getSourceBegin(qualifiedName);
                        // replace source
                        replaceSubstring(qualifiedName, shortClassName);
                        // replace node
                        {
                            ITypeBinding typeBinding = AstNodeUtils.getTypeBinding(qualifiedName);
                            SimpleName simpleName = m_parser.parseSimpleName(sourceBegin, shortClassName);
                            simpleName.setProperty(AstParser.KEY_TYPE_BINDING, typeBinding);
                            replaceNode(qualifiedName, simpleName);
                        }
                        // done
                        return false;
                    }
                }
                return true;
            }
        });
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // MethodInvocation operations
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Removes argument from {@link MethodInvocation}.
     */
    public void removeInvocationArgument(MethodInvocation invocation, int index) throws Exception {
        removeInvocationArgument(invocation, DomGenerics.arguments(invocation), index);
    }

    /**
     * Removes argument from {@link ClassInstanceCreation}.
     */
    public void removeCreationArgument(ClassInstanceCreation creation, int index) throws Exception {
        removeInvocationArgument(creation, DomGenerics.arguments(creation), index);
    }

    /**
     * Removes argument from {@link MethodInvocation} or {@link ClassInstanceCreation}.
     */
    private void removeInvocationArgument(ASTNode parent, List<Expression> arguments, int index) throws Exception {
        // prepare source interval to remove
        int sourceBegin;
        int sourceEnd;
        {
            Expression argument = arguments.get(index);
            sourceBegin = AstNodeUtils.getSourceBegin(argument);
            sourceEnd = AstNodeUtils.getSourceEnd(argument);
            if (index == 0) {
                if (arguments.size() == 1) {
                    sourceEnd = indexOf(")", sourceEnd);
                } else {
                    sourceEnd = indexOfAnyBut(", \t\r\n", sourceEnd);
                }
            } else {
                sourceBegin = indexOfAnyButBackward(", \t\r\n", sourceBegin) + 1;
            }
        }
        // remove node
        arguments.remove(index);
        // remove parameter type from binding
        DesignerMethodBinding methodBinding = getDesignerMethodBinding(parent);
        if (index < methodBinding.getParameterTypes().length) {
            // remove when not ellipsis argument
            methodBinding.removeParameterType(index);
        }
        // remove source
        replaceSubstring(sourceBegin, sourceEnd - sourceBegin, "");
    }

    /**
     * @return the new argument added to the {@link MethodInvocation}.
     */
    public Expression addInvocationArgument(MethodInvocation invocation, int index, String source)
            throws Exception {
        return addInvocationArgument(invocation, DomGenerics.arguments(invocation), index, source);
    }

    /**
     * @return the new argument added to the {@link ClassInstanceCreation}.
     */
    public Expression addCreationArgument(ClassInstanceCreation creation, int index, String source)
            throws Exception {
        return addInvocationArgument(creation, DomGenerics.arguments(creation), index, source);
    }

    /**
     * Move argument from position with index <code>oldIndex</code> to position with index
     * <code>newIndex</code>.
     */
    public Expression moveInvocationArgument(MethodInvocation invocation, int oldIndex, int newIndex)
            throws Exception {
        List<Expression> arguments = DomGenerics.arguments(invocation);
        // prepare move expression
        Expression expression = arguments.get(oldIndex);
        // prepare move source
        String source = getSource(expression);
        // remove from old array
        removeInvocationArgument(invocation, oldIndex);
        // add to new array
        int position = insertToInvocationBody(invocation, arguments, newIndex, source);
        // add node
        arguments.add(newIndex, expression);
        AstNodeUtils.moveNode(expression, position);
        return expression;
    }

    /**
     * Adds new argument {@link Expression} into {@link MethodInvocation} or
     * {@link ClassInstanceCreation}.
     * 
     * @param parent
     *          the {@link MethodInvocation} or {@link ClassInstanceCreation}.
     * @param arguments
     *          the {@link List} of argument {@link Expression}'s.
     * @param index
     *          the index for new argument.
     * @param source
     *          the source for new argument.
     * 
     * @return the new argument {@link Expression}.
     */
    private Expression addInvocationArgument(Expression parent, List<Expression> arguments, int index,
            String source) throws Exception {
        int position = insertToInvocationBody(parent, arguments, index, source);
        // add node
        Expression argument = getParser().parseExpression(position, source);
        arguments.add(index, argument);
        // replace binding for method
        replaceInvocationBinding(parent);
        // return added argument
        resolveImports(argument);
        return argument;
    }

    /**
     * Add to invocation body of source new argument with given index.
     * 
     * @return the start position of inserted code.
     */
    private int insertToInvocationBody(Expression parent, List<Expression> arguments, int index, String source)
            throws Exception {
        int position;
        String sourcePrefix = "";
        String sourceSuffix = "";
        if (index == 0) {
            if (arguments.size() == 0) {
                position = AstNodeUtils.getSourceEnd(parent) - 1;
            } else {
                Expression firstArgument = arguments.get(index);
                position = AstNodeUtils.getSourceBegin(firstArgument);
                sourceSuffix = ", ";
            }
        } else {
            Expression prevArgument = arguments.get(index - 1);
            position = AstNodeUtils.getSourceEnd(prevArgument);
            sourcePrefix = ", ";
        }
        // add source
        replaceSubstring(position, 0, sourcePrefix + source + sourceSuffix);
        position += sourcePrefix.length();
        return position;
    }

    /**
     * Replaces the arguments of given {@link ClassInstanceCreation}.
     * 
     * @param creation
     *          the {@link ClassInstanceCreation} to replace arguments.
     * @param lines
     *          the {@link String}'s of arguments. Number of lines is not related with number of
     *          arguments, lines are used to format code better.
     */
    public void replaceCreationArguments(ClassInstanceCreation creation, List<String> lines) throws Exception {
        replaceInvocationArguments(creation, creation.getType(), DomGenerics.arguments(creation), lines);
    }

    /**
     * Replaces the arguments of given {@link MethodInvocation}.
     * 
     * @param invocation
     *          the {@link MethodInvocation} to replace arguments.
     * @param lines
     *          the {@link String}'s of arguments. Number of lines is not related with number of
     *          arguments, lines are used to format code better.
     */
    public void replaceInvocationArguments(MethodInvocation invocation, List<String> lines) throws Exception {
        replaceInvocationArguments(invocation, invocation.getName(), DomGenerics.arguments(invocation), lines);
    }

    /**
     * Replaces the arguments of given {@link ClassInstanceCreation} with new arguments specified as
     * array of lines. Number of lines is not related with number of arguments, lines are used to
     * format code better.
     */
    /**
     * Replaces arguments of {@link MethodInvocation} or {@link ClassInstanceCreation}.
     * 
     * @param parent
     *          the {@link MethodInvocation} or {@link ClassInstanceCreation}.
     * @param arguments
     *          the {@link List} of <code>parent</code> arguments.
     * @param lines
     *          the {@link String}'s of arguments. Number of lines is not related with number of
     *          arguments, lines are used to format code better.
     */
    private void replaceInvocationArguments(Expression parent, ASTNode nameNode, List<Expression> arguments,
            List<String> lines) throws Exception {
        // prepare new arguments source
        String source;
        {
            // prepare code generation constants
            AstCodeGeneration generation = getGeneration();
            String singleIndent = generation.getIndentation(1);
            String eol = generation.getEndOfLine();
            //
            Statement statement = AstNodeUtils.getEnclosingStatement(parent);
            String indent = getWhitespaceToLeft(statement.getStartPosition(), false);
            source = getIndentedSource(lines, indent, singleIndent, eol);
            // remove indentation for first line
            source = source.trim();
        }
        // replace source
        int sourceBegin = indexOf("(", AstNodeUtils.getSourceBegin(nameNode)) + 1;
        int sourceEnd = AstNodeUtils.getSourceEnd(parent) - 1;
        replaceSubstring(sourceBegin, sourceEnd - sourceBegin, source);
        // replace binding
        ASTNode newInvocation = replaceInvocationBinding(parent);
        // replace arguments
        {
            List<Expression> newArguments = DomGenerics.arguments(newInvocation);
            List<Expression> newArgumentsCopy = new ArrayList<Expression>(newArguments);
            newArguments.clear();
            arguments.clear();
            arguments.addAll(newArgumentsCopy);
        }
        // finalize
        resolveImports(parent);
    }

    /**
     * Replaces the name of method in given {@link MethodInvocation}.
     */
    public void replaceInvocationName(MethodInvocation invocation, String newIdentifier) throws Exception {
        setIdentifier(invocation.getName(), newIdentifier);
        replaceInvocationBinding(invocation);
    }

    /**
     * Replaces the expression part in given {@link MethodInvocation}.
     */
    public void replaceInvocationExpression(MethodInvocation invocation, String newExpressionSource)
            throws Exception {
        if (invocation.getExpression() != null) {
            replaceExpression(invocation.getExpression(), newExpressionSource);
        } else {
            int position = invocation.getStartPosition();
            // insert Expression
            {
                Expression newExpression = getParser().parseExpression(position, newExpressionSource);
                replaceSubstring(position, 0, newExpressionSource + ".");
                invocation.setExpression(newExpression);
            }
            // we inserted Expression, update "begin" for parent nodes
            {
                int newInvocationPosition = invocation.getStartPosition();
                ASTNode node = invocation;
                while (node.getStartPosition() == newInvocationPosition) {
                    AstNodeUtils.setSourceBegin_keepEnd(node, position);
                    node = node.getParent();
                }
            }
        }
        replaceInvocationBinding(invocation);
    }

    /**
     * Parses the source of given {@link MethodInvocation} or {@link ClassInstanceCreation} and
     * installs its {@link IMethodBinding} as binding for given invocation.
     * 
     * @param invocation
     *          the {@link MethodInvocation} or {@link ClassInstanceCreation} to replace
     *          {@link IMethodBinding}.
     * 
     * @return the parsed {@link ASTNode}.
     */
    public ASTNode replaceInvocationBinding(Expression invocation) throws Exception {
        Assert.isLegal(invocation instanceof MethodInvocation || invocation instanceof ClassInstanceCreation);
        ASTNode parsedInvocation = m_parser.parseExpression(invocation.getStartPosition(), getSource(invocation));
        replaceMethodBinding(invocation, parsedInvocation);
        //
        return parsedInvocation;
    }

    /**
     * Replaces {@link IMethodBinding} in of existing {@link ASTNode} by using {@link IMethodBinding}
     * from equivalent parsed {@link ASTNode}.
     */
    private void replaceMethodBinding(ASTNode oldNode, ASTNode parsedNode) {
        IMethodBinding parsedBinding = (IMethodBinding) parsedNode.getProperty(AstParser.KEY_METHOD_BINDING);
        Assert.isTrue(parsedBinding instanceof DesignerMethodBinding);
        oldNode.setProperty(AstParser.KEY_METHOD_BINDING, parsedBinding);
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // ClassInstanceCreation += AnonymousClassDeclaration
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Adds {@link AnonymousClassDeclaration} into given {@link ClassInstanceCreation}.
     */
    public void addAnonymousClassDeclaration(ClassInstanceCreation creation) throws Exception {
        Assert.isNull2(creation.getAnonymousClassDeclaration(), "Already has anonymous: {0}", creation);
        // prepare indent
        String indent;
        {
            Statement statement = AstNodeUtils.getEnclosingStatement(creation);
            indent = getWhitespaceToLeft(statement.getStartPosition(), false);
        }
        // prepare positions
        int begin = AstNodeUtils.getSourceBegin(creation);
        int end = AstNodeUtils.getSourceEnd(creation);
        // prepare source
        String sourceNewCreation;
        {
            String eol = m_generation.getEndOfLine();
            String sourceInsert = " {" + eol + indent + "}";
            sourceNewCreation = getSource(creation) + sourceInsert;
            // insert source
            replaceSubstring(end, 0, sourceInsert);
        }
        // copy AnonymousClassDeclaration
        ClassInstanceCreation newCreation = (ClassInstanceCreation) getParser().parseExpression(begin,
                sourceNewCreation);
        AnonymousClassDeclaration newAnonymous = newCreation.getAnonymousClassDeclaration();
        newCreation.setAnonymousClassDeclaration(null);
        creation.setAnonymousClassDeclaration(newAnonymous);
        // we append {}, so replaceSubstring() will not update length, so we should update it manually 
        {
            ASTNode enclosingNode = creation;
            while (AstNodeUtils.getSourceEnd(enclosingNode) == end) {
                AstNodeUtils.setSourceEnd(enclosingNode, newAnonymous);
                enclosingNode = enclosingNode.getParent();
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Bindings
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * @return the new {@link DesignerTypeBinding} for given {@link ASTNode}, which has
     *         {@link ITypeBinding}.
     */
    private DesignerTypeBinding getDesignerTypeBinding(ASTNode node) {
        // prepare current binding
        ITypeBinding currentBinding;
        {
            currentBinding = AstNodeUtils.getTypeBinding((TypeDeclaration) node);
        }
        // set new DesignerTypeBinding
        DesignerTypeBinding designerBinding = m_bindingContext.getCopy(currentBinding);
        node.setProperty(AstParser.KEY_TYPE_BINDING, designerBinding);
        return designerBinding;
    }

    /**
     * @return existing or new {@link DesignerMethodBinding} for given {@link MethodInvocation} or
     *         {@link ClassInstanceCreation}. We use this {@link DesignerMethodBinding} for low-level
     *         modifications.
     */
    private DesignerMethodBinding getDesignerMethodBinding(ASTNode node) {
        // prepare current binding for method/constructor
        IMethodBinding currentMethodBinding;
        if (node instanceof MethodDeclaration) {
            currentMethodBinding = AstNodeUtils.getMethodBinding((MethodDeclaration) node);
        } else if (node instanceof MethodInvocation) {
            currentMethodBinding = AstNodeUtils.getMethodBinding((MethodInvocation) node);
        } else {
            currentMethodBinding = AstNodeUtils.getCreationBinding((ClassInstanceCreation) node);
        }
        // get existing DesignerMethodBinding, or create new wrapper
        if (currentMethodBinding instanceof DesignerMethodBinding) {
            return (DesignerMethodBinding) currentMethodBinding;
        } else {
            DesignerMethodBinding designerMethodBinding = m_bindingContext.get(currentMethodBinding);
            node.setProperty(AstParser.KEY_METHOD_BINDING, designerMethodBinding);
            return designerMethodBinding;
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Array
    //
    ////////////////////////////////////////////////////////////////////////////
    /**
     * Add element with given <code>index</code> to {@link ArrayInitializer}.
     */
    public Expression addArrayElement(ArrayInitializer arrayInitializer, int index, String source)
            throws Exception {
        // prepare source
        int position = insertToArrayBody(arrayInitializer, index, source);
        // add node
        Expression element = getParser().parseExpression(position, source);
        List<Expression> elements = DomGenerics.expressions(arrayInitializer);
        elements.add(index, element);
        // return added element
        resolveImports(element);
        return element;
    }

    /**
     * Move element form old array <code>oldArray</code> with index <code>oldIndex</code> to new array
     * <code>newArray</code> with index <code>newIndex</code>.
     */
    public Expression moveArrayElement(ArrayInitializer oldArray, ArrayInitializer newArray, int oldIndex,
            int newIndex) throws Exception {
        // prepare move expression
        Expression expression = DomGenerics.expressions(oldArray).get(oldIndex);
        // prepare move source
        String source = getSource(expression);
        // remove from old array
        removeArrayElement(oldArray, oldIndex);
        // add to new array
        int position = insertToArrayBody(newArray, newIndex, source);
        // add node
        DomGenerics.expressions(newArray).add(newIndex, expression);
        AstNodeUtils.moveNode(expression, position);
        return expression;
    }

    /**
     * Add to array elements body of source new element with given index.
     * 
     * @return the start position of inserted code.
     */
    private int insertToArrayBody(ArrayInitializer arrayInitializer, int index, String source) throws Exception {
        // prepare elements
        List<Expression> elements = DomGenerics.expressions(arrayInitializer);
        int position;
        String sourcePrefix = "";
        String sourceSuffix = "";
        if (index == 0) {
            if (elements.size() == 0) {
                position = AstNodeUtils.getSourceEnd(arrayInitializer) - 1;
            } else {
                Expression firstElement = elements.get(index);
                position = AstNodeUtils.getSourceBegin(firstElement);
                sourceSuffix = ", ";
            }
        } else {
            Expression prevElement = elements.get(index - 1);
            position = AstNodeUtils.getSourceEnd(prevElement);
            sourcePrefix = ", ";
        }
        // add source
        replaceSubstring(position, 0, sourcePrefix + source + sourceSuffix);
        position += sourcePrefix.length();
        //
        return position;
    }

    /**
     * Remove element with given <code>index</code> from {@link ArrayInitializer}.
     */
    public void removeArrayElement(ArrayInitializer arrayInitializer, int index) throws Exception {
        // prepare elements
        List<Expression> elements = DomGenerics.expressions(arrayInitializer);
        if (index >= elements.size()) {
            return;
        }
        // prepare source interval to remove
        int sourceBegin;
        int sourceEnd;
        {
            Expression element = elements.get(index);
            sourceBegin = AstNodeUtils.getSourceBegin(element);
            sourceEnd = AstNodeUtils.getSourceEnd(element);
            if (index == 0) {
                if (elements.size() == 1) {
                    sourceEnd = indexOf("}", sourceEnd);
                } else {
                    sourceEnd = indexOfAnyBut(", \t\r\n", sourceEnd);
                }
            } else {
                sourceBegin = indexOfAnyButBackward(", \t\r\n", sourceBegin) + 1;
            }
        }
        // remove node
        elements.remove(index);
        // remove source
        replaceSubstring(sourceBegin, sourceEnd - sourceBegin, "");
    }

    /**
     * Exchanges elements with given indexes inside of {@link ArrayInitializer}.
     */
    public void exchangeArrayElements(ArrayInitializer arrayInitializer, int index_1, int index_2)
            throws Exception {
        // prepare elements
        List<Expression> elements = DomGenerics.expressions(arrayInitializer);
        Expression element_1 = elements.get(index_1);
        Expression element_2 = elements.get(index_2);
        String source_1 = getSource(element_1);
        String source_2 = getSource(element_2);
        int position_1 = element_1.getStartPosition();
        int position_2 = element_2.getStartPosition();
        int length_1 = element_1.getLength();
        int length_2 = element_2.getLength();
        // exchange elements
        {
            elements.set(index_1, arrayInitializer.getAST().newSimpleName("foo_1"));
            elements.set(index_2, arrayInitializer.getAST().newSimpleName("foo_2"));
            // set source
            if (position_1 < position_2) {
                replaceSubstring(position_2, length_2, source_1);
                replaceSubstring(position_1, length_1, source_2);
                position_2 += length_2 - length_1;
            } else {
                replaceSubstring(position_1, length_1, source_2);
                replaceSubstring(position_2, length_2, source_1);
                position_1 += length_1 - length_2;
            }
            // set positions
            AstNodeUtils.moveNode(element_1, position_2);
            AstNodeUtils.moveNode(element_2, position_1);
            // set nodes
            elements.set(index_1, element_2);
            elements.set(index_2, element_1);
        }
    }
}