/******************************************************************************* * Copyright (c) 2012 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 com.windowtester.eclipse.ui.convert; import static com.windowtester.eclipse.ui.convert.util.WTAPIUtil.simpleTypeName; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.ImportDeclaration; import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.core.dom.Name; import org.eclipse.jdt.core.dom.QualifiedName; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.SimpleType; /** * The source, AST parse tree, and type information for the compilation unit being * modified. Use {@link WTConvertAPIContextBuilder} to build this context. * <ul> * <li>When calling discrete methods such as {@link ImportDeclaration#setName(Name)}, call * {@link #replacing(ASTNode, ASTNode)} before calling the discrete method.</li> * <li>When inserting one or more nodes into a list such as the list returned by * {@link CompilationUnit#imports()}, call {@link #insert(List, int, ASTNode)} or * {@link #insert(List, int, Collection)} rather than modifying the list directly</li> * <li></li> * <li></li> * <li></li> * <li></li> * <li></li> * </ul> * Because imports affect the types visible to the rest of the compilation unit, they have * their own set of special methods that should be called rather than calling the methods * listed above. * <ul> * <li>When adding a new import, call {@link #addImport(String, boolean)} rather than * calling {@link #insert(List, int, ASTNode)}</li> * <li>When replacing an existing import with a new import, call * {@link #replaceImport(ImportDeclaration, String)} rather than calling * {@link #replacing(ASTNode, ASTNode)} and {@link ImportDeclaration#setName(Name)}</li> * <li>When removing an import, call {@link #removeImport(ImportDeclaration)} rather than * calling {@link #remove(List, ASTNode)}</li> * <li></li> * <li></li> * </ul> */ public class WTConvertAPIContext { private String source; private final String originalSource; private final CompilationUnit compUnit; /** * A collection of fully qualified names of WindowTester types imported in this * compilation unit */ private final Set<String> wtTypeNames; /** * A map of simple name to fully qualified name of WindowTester types imported in this * compilation unit */ private final Map<String, String> wtSimpleTypeNameMap; /** * A collection of fully qualified names of WindowTester type members statically * imported in this compilation unit */ private final Set<String> wtStaticTypeNames; /** * A map of simple name to fully qualified name of WindowTester types members * statically imported in this compilation unit */ private final HashMap<String, String> wtSimpleStaticTypeNameMap; /** * A collection of names of types to be removed at the end of the * {@link #accept(ASTVisitor)} processing. */ private final Collection<String> wtTypeNamesToRemove; /** * A collection of fully qualified names of common types (e.g. types in the java.lang * package) */ private static final Set<String> COMMON_TYPE_NAMES = new HashSet<String>(); /** * A mapping of simple type name to fully qualified type name for common types that * are not WindowTester types (e.g. types in the java.lang package). This mapping is * statically initialized and also fed by types referenced in various rules. * * @see #addCommonType(Class) */ private static final Map<String, String> COMMON_SIMPLE_TYPE_NAME_MAP = new HashMap<String, String>(); /** * Initialize the common type static fields */ static { // TODO add additional common types addCommonType(Integer.class); addCommonType(Integer.TYPE); addCommonType(Object.class); addCommonType(String.class); } /** * Add a common type that is not a WindowTester type (e.g. java.lang.Object, * org.eclipse.swt.widgets.Shell) to the list of known types for use during WT API * migration. * * @param type the type (not <code>null</code>) */ public static void addCommonType(Class<?> type) { COMMON_TYPE_NAMES.add(type.getName()); COMMON_SIMPLE_TYPE_NAME_MAP.put(type.getSimpleName(), type.getName()); } /** * Construct a new instance for traversing and manipulating the compilation unit * * @param source the compilation unit source (not <code>null</code>) * @param compUnit the compilation unit (not <code>null</code>) * @param wtTypeNames the WindowTester types imported by this compilation unit (not * <code>null</code>) * @param wtStaticTypeNames the WindowTester static members imported by this * compilation unit (not <code>null</code>) */ public WTConvertAPIContext(String source, CompilationUnit compUnit, Set<String> wtTypeNames, Set<String> wtStaticTypeNames) { this.source = source; this.originalSource = source; this.compUnit = compUnit; this.wtTypeNames = wtTypeNames; wtSimpleTypeNameMap = new HashMap<String, String>(); for (String typeName : wtTypeNames) wtSimpleTypeNameMap.put(simpleTypeName(typeName), typeName); this.wtStaticTypeNames = wtStaticTypeNames; wtSimpleStaticTypeNameMap = new HashMap<String, String>(); for (String typeName : wtStaticTypeNames) wtSimpleStaticTypeNameMap.put(simpleTypeName(typeName), typeName); wtTypeNamesToRemove = new ArrayList<String>(); } //=========================================================== // Accessors /** * Answer <code>true</code> if the source has been modified */ public boolean isSourceModified() { return !source.equals(originalSource); } /** * Answer the modified source */ public String getSource() { return source; } /** * Answer the modified source for the specified node * * @param node the node (must be part of the receiver) * @return the source */ public String getSourceFor(ASTNode node) { int start = node.getStartPosition(); return source.substring(start, start + node.getLength()); } /** * Answer the AST parse tree */ public CompilationUnit getCompilationUnit() { return compUnit; } /** * Answer true if the receiver's compilation unit references WindowTester types */ public boolean hasWTReferences() { return wtTypeNames.size() > 0 || wtStaticTypeNames.size() > 0; } /** * Answer the names of the imported types */ public Set<String> getWTTypeNames() { return wtTypeNames; } /** * Answer the fully qualified names of the WindowTester types in the specified package * or <code>null</code> if the specified package is not part of the WindowTester * product */ public Collection<String> getWTTypesInPackage(String packageName) { return WTConvertAPIContextBuilder.getWTTypesInPackage(packageName); } /** * Answer the fully qualified type name if it can be resolved or <code>null</code> if * it cannot be resolved * * @param typeName the simple or fully qualified name of the type * @return the fully qualified name of the type or <code>null</code> if cannot be * resolved */ public String resolve(String typeName) { if (typeName == null) return null; if (typeName.indexOf('.') != -1) return typeName; int index = typeName.indexOf('['); String suffix = ""; if (index != -1) { suffix = typeName.substring(index); typeName = typeName.substring(0, index); } String resolvedTypeName = wtSimpleTypeNameMap.get(typeName); if (resolvedTypeName == null) resolvedTypeName = COMMON_SIMPLE_TYPE_NAME_MAP.get(typeName); return resolvedTypeName + suffix; } /** * Determine if the specified name is the name of a WindowTester type */ public boolean isWTType(String typeName) { if (typeName == null) return false; if (typeName.endsWith(".class")) typeName = typeName.substring(0, typeName.length() - 6); if (typeName.indexOf('.') == -1) return wtSimpleTypeNameMap.containsKey(typeName); return WTConvertAPIContextBuilder.isWTType(typeName); } /** * Traverse the compilation unit's parse tree * * @param visitor the visitor (not <code>null</code>) */ public void accept(ASTVisitor visitor) { compUnit.accept(visitor); // Remove imports that were replaced using the replaceImport(...) method wtTypeNames.removeAll(wtTypeNamesToRemove); for (String typeName : wtTypeNamesToRemove) wtSimpleTypeNameMap.remove(simpleTypeName(typeName)); } //=========================================================== // Imports /** * Adds an import for the given type to the beginning of the list of imports. * * @param newTypeName the fully qualified type name to be imported (not * <code>null</code>, not empty) * @param isStatic <code>true</code> if the import should be a static import */ @SuppressWarnings("unchecked") public void addImport(String newTypeName, boolean isStatic) { //NOTE: this assumes that there is at least one import... TODO: fix this! List<ASTNode> imports = getCompilationUnit().imports(); for (ASTNode node : imports) { ImportDeclaration importNode = (ImportDeclaration) node; String typeName = importNode.getName().getFullyQualifiedName(); if (typeName.equals(newTypeName) && importNode.isStatic() == isStatic) return; } insert(compUnit, imports, 0, newImport(newTypeName, isStatic)); addKnownType(newTypeName, isStatic); } /** * Expand the specified demand import to be zero or more explicit imports * * @param importNode the demand import to be expanded */ @SuppressWarnings("unchecked") public void expandImport(ImportDeclaration importNode) { assertInCompUnit(importNode); if (!importNode.isOnDemand()) throw new IllegalArgumentException("import is not on demand: " + importNode); String packageName = importNode.getName().getFullyQualifiedName(); Collection<String> wtTypeNames = getWTTypesInPackage(packageName); if (wtTypeNames == null) return; Collection<ASTNode> newNodes = new ArrayList<ASTNode>(); for (String typeName : new TreeSet<String>(wtTypeNames)) newNodes.add(newImport(typeName, false)); CompilationUnit compUnit = (CompilationUnit) importNode.getParent(); List<ASTNode> imports = compUnit.imports(); int index = imports.indexOf(importNode); // Intentionally do not use the add/remove import methods // because we are not changing the list of visible types insert(compUnit, imports, index, newNodes); remove(imports, importNode); } /** * Modify the specified import declaration to import the specified type * * @param importNode the import declaration to be modified * @param newTypeName the fully qualified type name to be imported (not * <code>null</code>, not empty) */ public void replaceImport(ImportDeclaration importNode, String newTypeName) { assertInCompUnit(importNode); Name oldNameNode = importNode.getName(); String oldTypeName = oldNameNode.getFullyQualifiedName(); Name newNameNode = newName(newTypeName, 0); replacing(oldNameNode, newNameNode); importNode.setName(newNameNode); removeKnownType(oldTypeName, importNode.isStatic()); addKnownType(newTypeName, importNode.isStatic()); } /** * Remove the import from the current list of imports * * @param importNode the import to be removed */ @SuppressWarnings("unchecked") public void removeImport(ImportDeclaration importNode) { assertInCompUnit(importNode); CompilationUnit cu = getCompilationUnit(); List<ASTNode> imports = cu.imports(); Name oldNameNode = importNode.getName(); String oldTypeName = oldNameNode.getFullyQualifiedName(); remove(imports, importNode); removeKnownType(oldTypeName, importNode.isStatic()); } /** * Construct a new import declaration * * @param typeName the fully qualified type name (not <code>null</code>, not empty) * @param isStatic <code>true</code> if the import should be a static import * @return the import declaration (not <code>null</code>) */ private ImportDeclaration newImport(String typeName, boolean isStatic) { ImportDeclaration newImport = compUnit.getAST().newImportDeclaration(); newImport.setStatic(isStatic); int start = 7; // 7 = position after the "import" keyword and one space if (isStatic) start += 7; // add length of "static" keyword and one space newImport.setName(newName(typeName, start)); newImport.setSourceRange(0, newImport.toString().trim().length()); return newImport; } /** * Add the specified type to the list of known types * * @param newTypeName the fully qualified type name to be added (not <code>null</code> * , not empty) * @param isStatic <code>true</code> if the import should be a static import */ private void addKnownType(String newTypeName, boolean isStatic) { if (isStatic) { // TODO How do we cache statically imported types? } else { wtTypeNames.add(newTypeName); wtSimpleTypeNameMap.put(simpleTypeName(newTypeName), newTypeName); } } /** * Cannot remove old type immediately because it may be needed by the resolve(...) * method so queue the old type for removal at the end of the accept(...) method * * @param oldTypeName the fully qualified type name to be removed * @param isStatic <code>true</code> if the import is a static import */ private void removeKnownType(String oldTypeName, boolean isStatic) { if (isStatic) { // TODO How do we cache statically imported types? } else { wtTypeNamesToRemove.add(oldTypeName); } } //================================================================================= // Node modification utility methods /** * Change the name of the type being referenced * * @param type the type to be changed (not <code>null</code> and must be in the * receiver's compilation unit) * @param the new type name (not <code>null</code>, not empty) */ public void setTypeName(SimpleType type, String newTypeName) { Name newNameNode = newName(newTypeName, 0); replacing(type.getName(), newNameNode); type.setName(newNameNode); } /** * Chnage the name of the method being invoked * * @param invocation the method invocation (not <code>null</code> and must be in the * receiver's compilation unit) * @param newMethodName the new name of the method to be invoked (not * <code>null</code>, not empty) */ public void setMethodName(MethodInvocation invocation, String newMethodName) { SimpleName newName = newSimpleName(newMethodName, 0); replacing(invocation.getName(), newName); invocation.setName(newName); } //================================================================================= // Inserting and removing nodes in a list /** * Insert the new node in the specified list of nodes. WARNING! this method ASSUMES * that nodeList contains at least one element * * @param parent the node containing the node list * @param nodeList the list of nodes (not <code>null</code> and contains no * <code>null</code>s) * @param index the position at which the node should be inserted (0 <= index <= * nodeList.length()) * @param newNode the new node (not <code>null</code>) */ public void insert(ASTNode parent, List<ASTNode> nodeList, int index, ASTNode newNode) { List<ASTNode> newNodes = new ArrayList<ASTNode>(); newNodes.add(newNode); insert(parent, nodeList, index, newNodes); } /** * Insert the new nodes in the specified list of nodes. WARNING! this method ASSUMES * that nodeList contains at least one element because otherwise we cannot determine a * starting position. * * @param parent the node containing the node list * @param nodeList the list of nodes (not <code>null</code> and contains no * <code>null</code>s) * @param index the position at which the nodes should be inserted (0 <= index <= * nodeList.length()) * @param newNodes the new nodes (not <code>null</code> and contains no * <code>null</code> s) */ public void insert(ASTNode parent, List<ASTNode> nodeList, int index, Collection<ASTNode> newNodes) { assertInCompUnit(parent); // Find the starting position in the source if possible int start; if (nodeList.size() == 0) { if (parent instanceof MethodInvocation) { MethodInvocation invocation = (MethodInvocation) parent; SimpleName methodName = invocation.getName(); start = methodName.getStartPosition() + methodName.getLength(); while (source.charAt(start) != '(') start++; start++; } // TODO add section to determine start when inserting imports into a compilation unit without imports else throw new IllegalStateException("Cannot determine start because nodeList is empty"); } else if (index == nodeList.size()) { ASTNode lastNode = nodeList.get(index - 1); start = lastNode.getStartPosition() + lastNode.getLength(); } else { start = nodeList.get(index).getStartPosition(); } // Insert the nodes and adjust the source String newSource = ""; for (ASTNode node : newNodes) { assertRoot(node); adjustStartPositions(node, 0, start + newSource.length()); newSource += node.toString(); } adjustStartPositions(compUnit, start, start + newSource.length()); for (ASTNode node : newNodes) { nodeList.add(index++, node); } source = source.substring(0, start) + newSource + source.substring(start); } /** * Remove the specified node from the specified list of nodes * * @param nodeList the list of nodes (not <code>null</code> and contains no * <code>null</code>s) * @param oldNode the node to be removed (not <code>null</code>) * @return */ public ASTNode remove(List<ASTNode> nodeList, ASTNode oldNode) { int index = nodeList.indexOf(oldNode); if (index == -1) throw new IllegalArgumentException("The nodeList does not contain the node to be removed"); return remove(nodeList, index); } /** * Remove the specified node from the specified list of nodes. * * @param nodeList the list of nodes (not <code>null</code> and contains no * <code>null</code>s) * @param index the index of the node to be removed (0 <= index < nodeList.size()) */ public ASTNode remove(List<ASTNode> nodeList, int index) { ASTNode oldNode = nodeList.get(index); assertInCompUnit(oldNode); // Find the original node's source start and end. int start = oldNode.getStartPosition(); int oldEnd = start + oldNode.getLength(); // If this is a statement (ends with a semi-colon) // then consume the whitespace to the end of the line including the line end characters if (source.charAt(oldEnd - 1) == ';') { oldEnd = skipWhitespaceToNextLineStart(oldEnd); } // otherwise consume trailing comma, or leading comma if part of a list of method arguments else { oldEnd = skipWhitespace(oldEnd); if (source.charAt(oldEnd) == ',') oldEnd = skipWhitespace(oldEnd + 1); // TODO consume leading comma if trailing comma is not found } // Remove the node and adjust the source adjustStartPositions(oldNode, start, 0); adjustStartPositions(compUnit, oldEnd, start); source = source.substring(0, start) + source.substring(oldEnd); nodeList.remove(index); return oldNode; } //================================================================ // AST node construction /** * Answer a copy of the specified node or <code>null</code> if the specified node is null */ public ASTNode deepCopy(ASTNode node) { if (node == null) return null; ASTNode result = ASTNode.copySubtree(compUnit.getAST(), node); adjustStartPositions(result, result.getStartPosition(), 0); return result; } /** * Construct a new simple name * * @param text the simple name as text (not <code>null</code>, not empty, not a * keyword, not boolean literal ("true", "false"), not null literal * ("null") * @param startPosition the starting position relative to the new AST nodes being * created. For example, when instantiating a new name node to replace an * existing name node, pass zero. When instantiation a new name node to be * used as part of a new import declaration, pass the offset of the name * node within the new import declaration. * @return a new name (not <code>null</code>) */ public SimpleName newSimpleName(String text, int startPosition) { SimpleName newName = compUnit.getAST().newSimpleName(text); newName.setSourceRange(startPosition, text.length()); return newName; } /** * Construct a new simple or qualified name * * @param text the simple or qualified name as text (not <code>null</code>, not empty) * @param startPosition the starting position relative to the new AST nodes being * created. For example, when instantiating a new name node to replace an * existing name node, pass zero. When instantiation a new name node to be * used as part of a new import declaration, pass the offset of the name * node within the new import declaration. * @return a new name (not <code>null</code>) */ public Name newName(String text, final int startPosition) { Name newName = compUnit.getAST().newName(text); newName.accept(new ASTVisitor() { int index = startPosition; public boolean visit(QualifiedName node) { node.setSourceRange(startPosition, node.toString().length()); return true; } public boolean visit(SimpleName node) { int length = node.getIdentifier().length(); node.setSourceRange(index, length); index += length + 1; return true; } }); return newName; } /** * Construct a new method invocation * * @param target the target expression or null if none * @param methodName the method name (not <code>null</code>, not empty) * @param arguments the argument expressions * @return the method invocation */ @SuppressWarnings("unchecked") public MethodInvocation newMethodInvocation(Expression target, String methodName, Expression... arguments) { MethodInvocation invocation = getCompilationUnit().getAST().newMethodInvocation(); int start = 0; if (target != null) { assertRoot(target); invocation.setExpression(target); start += target.toString().length() + 1; // target plus dot } invocation.setName(newSimpleName(methodName, start)); start += methodName.length(); for (int i = 0; i < arguments.length; i++) { Expression arg = arguments[i]; assertRoot(arg); start += i == 0 ? 1 : 2; // opening parenthesis or comma and space adjustStartPositions(arg, 0, start); invocation.arguments().add(arg); start += arg.toString().length(); } invocation.setSourceRange(0, invocation.toString().length()); return invocation; } //================================================================ // Internal utility methods /** * Find the start of the next line */ private int skipWhitespace(int index) { while (index < source.length()) { if (!Character.isWhitespace(source.charAt(index))) break; index++; } return index; } /** * Find next non-whitespace character or the start of the next line whichever comes * first */ private int skipWhitespaceToNextLineStart(int index) { while (index < source.length()) { char ch = source.charAt(index++); if (!Character.isWhitespace(ch)) break; if (ch == '\r') { if (index < source.length() && source.charAt(index) == '\n') index++; break; } if (ch == '\n') { break; } } return index; } /** * Call this method to update the source BEFORE one node replaces another node * * @param oldNode the original node (not <code>null</code>) * @param newNode the replacement node (not <code>null</code>) */ private void replacing(ASTNode oldNode, ASTNode newNode) { assertNotRoot(oldNode); assertRoot(newNode); // Find the original node's source int start = oldNode.getStartPosition(); int oldEnd = start + oldNode.getLength(); // Adjust the source adjustStartPositions(oldNode, start, 0); adjustStartPositions(newNode, 0, start); adjustStartPositions(compUnit, oldEnd, start + newNode.getLength()); source = source.substring(0, start) + newNode.toString() + source.substring(oldEnd); } /** * Adjust the start position of nodes with start position greater than oldStart in the * specified root node. * * @param startNode the top most node containing nodes to be adjusted * @param oldStart the old start position * @param newStart the new start position */ private void adjustStartPositions(ASTNode startNode, final int oldStart, int newStart) { final int offset = newStart - oldStart; startNode.accept(new ASTVisitor() { public void preVisit(ASTNode node) { if (node.getStartPosition() >= oldStart) node.setSourceRange(node.getStartPosition() + offset, node.getLength()); else if (node.getStartPosition() + node.getLength() >= oldStart) node.setSourceRange(node.getStartPosition(), node.getLength() + offset); } }); } /** * Answer the root node containing the specified node or return the specified node if * the specified node is a root node * * @param node the node in question * @return the root node (not <code>null</code>) */ private ASTNode getRoot(ASTNode node) { ASTNode current = node; while (true) { ASTNode parent = current.getParent(); if (parent == null) return current; current = parent; } } /** * Assert that the specified node is a root node and not a child of any other node * * @param node the node to be checked (not <code>null</code>) */ private void assertRoot(ASTNode node) { if (node.getParent() != null) throw new IllegalArgumentException("Expected root node: " + node); } /** * Assert that the specified node is not a root node * * @param node the node to be checked (not <code>null</code>) */ private void assertNotRoot(ASTNode node) { if (node.getParent() == null) throw new IllegalArgumentException("Expected non-root node: " + node); } /** * Assert that the specified node is part of the receiver's compilation unit * * @param node the node to check (not <code>null</code>) */ private void assertInCompUnit(ASTNode node) { if (getRoot(node) != compUnit) throw new IllegalArgumentException("Expected node to be part of compilation unit: " + node); } }