/** * Copyright (c) 2006 IBM Corporation and others. * 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: * IBM - Initial API and implementation */ package org.eclipse.emf.codegen.merge.java.facade.ast; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor; import org.eclipse.jdt.core.dom.ChildPropertyDescriptor; import org.eclipse.jdt.core.dom.SimplePropertyDescriptor; import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; import org.eclipse.jdt.core.dom.rewrite.ListRewrite; import org.eclipse.jdt.core.dom.rewrite.TargetSourceRangeComputer; import org.eclipse.emf.codegen.merge.java.facade.AbstractJNode; import org.eclipse.emf.codegen.merge.java.facade.FacadeHelper; import org.eclipse.emf.codegen.merge.java.facade.JNode; /** * Each subclass of <code>ASTJNode</code> wraps subclass of {@link ASTNode}. * <p> * <code>ASTJNode</code> may have reference to multiple <code>ASTNode</code> objects. * <p> * Wrapped <code>ASTNode</code> (returned by {@link #getWrappedObject()}) is always the node in the rewritten tree. Each object of <code>ASTJNode</code> * wraps a unique <code>ASTNode</code>. After nodes are removed, wrapped <code>ASTNode</code> has reference to the move target node that allows * the node to be inserted after, i.e. perform moving of the node. * <p> * Original ASTNode (returned by {@link #getASTNode()}) is used to make modifications to the node and its children. This node must * be used by get and set methods. Using this node allows to make modifications to the nodes after * they are moved. * <p> * Some subclasses (e.g. <code>ASTJAnnotation</code> and <code>ASTJField</code>) may need a different <code>ASTNode</code> to be used * by set methods. These subclasses use <code>getASTNode()</code> only for get methods. * <p> * Removed ASTNode (returned by {@link #getRemovedASTNode()}) is used to keep reference to the node * that was removed. This allows to notify the range computer when the removed node is inserted again. * <p> * * @see #getWrappedObject() * @see #getASTNode() * @see #getRemovedASTNode() * * @param <T> wrapped AST node type * * @since 2.2.0 */ public abstract class ASTJNode<T extends ASTNode> extends AbstractJNode { /** * Default value used as default value of fields of subclasses. * Indicates that fields have not been initialized. * This value should never be returned by any get...() methods. */ protected static final String UNITIALIZED_STRING = "UNITIALIZED_STRING"; /** * Name of the node. * * @see JNode#getName() * @see JNode#setName(String) */ protected String name = UNITIALIZED_STRING; /** * <code>true</code> if the node is commented out, <code>false</code> otherwise * @see #commentOut() */ protected boolean isCommentedOut = false; private ASTFacadeHelper facadeHelper; /** * AST node used to read contents and make modifications to the nodes. */ private T astNode = null; private ASTNode wrappedObject; /** * Flag that determines if parent property has been set * (if not, <code>ASTNode.getParent()</code> will be used to retrieve the parent). */ private boolean isParentSet = false; /** * Current parent of ASTJNode. * <p> * This attribute should used instead of ASTNode parent attribute. In some situations * (i.e. moving nodes by removing and inserting) ASTNode parent attribute * might not be updated. In addition, for the cloned nodes ASTNode parent attribute might * not be set either. * <p> * When ASTJNode is deleted, parent is set to <code>null</code>.<p> */ private ASTJNode<?> parent = null; /** * Reference to the original AST node that was removed from the tree. * <p> * This reference is used to notify the source range computer when the * removed node is inserted again (i.e. node is moved). */ private ASTNode removedASTNode = null; /** * ASTRewrite object used to keep track of all modifications */ protected ASTRewrite rewriter = null; /** * Map of nodes to their contents. This map may only contain wrapped node and/or its children. * This map is used to preserve the contents when node is removed and then inserted. * * @see ASTJCompilationUnit#getAllTrackedContentsMap() */ private Map<ASTNode, String> trackedContentsMap = new HashMap<ASTNode, String>(4); /** * @see AbstractJNode#AbstractJNode() * @param astNode to be used as wrapped object */ protected ASTJNode(T astNode) { this.astNode = astNode; wrappedObject = astNode; } @Override public void dispose() { rewriter = null; trackedContentsMap.clear(); wrappedObject = null; removedASTNode = null; astNode = null; facadeHelper = null; name = null; } @Override public boolean isDisposed() { return rewriter == null; } @Override public ASTFacadeHelper getFacadeHelper() { return facadeHelper; } @Override public void setFacadeHelper(FacadeHelper facadeHelper) { this.facadeHelper = (ASTFacadeHelper)facadeHelper; } /** * Returns AST node used to read contents and make modifications to the nodes. * <p> * When the node is removed, the wrapped node returned by {@link #getWrappedObject()} will be * a place-holder node, while this method will return the original node that can be modified by * <code>set...</code> methods. * * @return AST node */ protected T getASTNode() { return astNode; } /** * @param astNode the astNode to set */ protected void setASTNode(T astNode) { this.astNode = astNode; } /** * Returns wrapped AST node that is currently in the rewritten tree, or to be inserted into * the tree. This node can be a move target node or any other temporary node. * Methods such as <code>set...</code> and <code>get...</code> should not use this node. * * @return wrapped AST node * @see #getASTNode() * @see #getRemovedASTNode() */ @Override protected ASTNode getWrappedObject() { return wrappedObject; } /** * Sets the wrapped object to the given <code>ASTNode</code>. Must be * used with caution and only by subclasses. * @param node * @see #getWrappedObject() */ protected void setWrappedObject(ASTNode node) { wrappedObject = node; } /** * @return the parent of this ASTJNode, or <code>null</code> if this node has been created or removed */ public ASTJNode<?> getParent() { if (parent == null && !isParentSet) { setParent(facadeHelper.findParent(getWrappedObject())); } return parent; } /** * Sets the parent of this node. * * @param parent the parent to set, <code>null</code> if the node does not have a parent */ public void setParent(ASTJNode<?> parent) { isParentSet = true; this.parent = parent; } /** * @return the rewriter */ public ASTRewrite getRewriter() { return rewriter; } /** * @param rewriter the rewriter to set */ public void setRewriter(ASTRewrite rewriter) { this.rewriter = rewriter; } /** * Adds a child to this node. * <p> * Default implementation does nothing and returns <code>false</code>. * * @param child to add * @return <code>true</code> if operation successful, <code>false</code> otherwise */ public boolean addChild(ASTJNode<?> child) { return false; } /** * Adds <code>value</code> to the property of the node. * * @param node * @param value * @param property */ protected void addValueToListProperty(ASTNode node, ASTNode value, ChildListPropertyDescriptor property) { if (value == null) { return; } if (ASTFacadeHelper.DEBUG) { facadeHelper.logInfo("Adding value to list property <" + value + ">"); } ListRewrite listRewrite = rewriter.getListRewrite(node, property); listRewrite.insertLast(value, null); } /** * Adds <code>value</code> as a new string place-holder to the <code>property</code> of the <code>node</code>. * <p> * If <code>value</code> is <code>null</code>, no changes are made. * * @param node * @param value * @param property * @param nodeType of the place-holder */ protected void addValueToListProperty(ASTNode node, String value, ChildListPropertyDescriptor property, int nodeType) { if (value == null) { return; } if (ASTFacadeHelper.DEBUG) { facadeHelper.logInfo("Adding value to list property <" + value + ">"); } ListRewrite listRewrite = rewriter.getListRewrite(node, property); listRewrite.insertLast(rewriter.createStringPlaceholder(value, nodeType), null); } /** * Notifies the node that one of ancestors in the hierarchy or this node itself was inserted. * This means that this node was inserted as well. * <p> * This implementation enables tracked contents for this node and calls itself on all of the children. */ protected void ancestorInserted() { enableTrackAndReplace(); for (JNode node : getChildren()) { ((ASTJNode<?>)node).ancestorInserted(); } } /** * Notifies the node that one of ancestors in the hierarchy or this node itself will be removed. * This means that this node will be removed as well. * <p> * This implementation disables tracked contents for this node and calls itself on all of the children. */ protected void ancestorToBeRemoved() { disableTrackAndReplace(); for (JNode node : getChildren()) { ((ASTJNode<?>)node).ancestorToBeRemoved(); } } /** * Notifies the node that the child will be changed. * <p> * Parents that are interested in changes to children must override this method. * Children that need to notify the parents about changes have to call this method. * <p> * Default implementation does nothing. * * @param child that will be changed */ protected void childToBeChanged(ASTJNode<?> child) { // default implementation does nothing } /** * Convert a given list of nodes to an array of strings. * <p> * All nodes in the given array must have valid source range and belong to the original tree. * * @param nodes * @return string array * * @see ASTFacadeHelper#toString(ASTNode) */ protected String[] convertASTNodeListToStringArray(List< ? extends ASTNode> nodes) { if (nodes == null) { return EMPTY_STRING_ARRAY; } String[] strings = new String [nodes.size()]; int i = 0; for (ASTNode node : nodes) { strings[i++] = facadeHelper.toString(node); } return strings; } /** * Temporarily disables tracking for all AST nodes that are tracked by this <code>ASTJNode</code>. * <p> * Used when modified <code>ASTJNode</code>s are removed from the tree, and then possibly inserted again. * * @see #enableTrackAndReplace() * @see #trackAndReplace(ASTNode, String) */ protected void disableTrackAndReplace() { ASTJCompilationUnit compilationUnit = (ASTJCompilationUnit)facadeHelper.getCompilationUnit(this); if (compilationUnit != null) { for (ASTNode node : trackedContentsMap.keySet()) { compilationUnit.getAllTrackedContentsMap().remove(node); } // disable commenting out if (isCommentedOut) { compilationUnit.getCommentedOutNodes().remove(getASTNode()); } } } /** * Enables disabled tracking for all AST nodes that are tracked by this <code>ASTJNode</code>. * <p> * Used when modified <code>ASTJNode</code>s are removed from the tree, and then inserted again. * * @see #disableTrackAndReplace() * @see #trackAndReplace(ASTNode, String) */ protected void enableTrackAndReplace() { ASTJCompilationUnit compilationUnit = (ASTJCompilationUnit)facadeHelper.getCompilationUnit(this); if (compilationUnit != null) { compilationUnit.getAllTrackedContentsMap().putAll(trackedContentsMap); // enable commenting out if (isCommentedOut) { compilationUnit.getCommentedOutNodes().add(getASTNode()); } } } /** * Wraps up inserting new node. Notifies the children and the range computer that the node is inserted. * * @param newNode */ private void finishInsert(ASTJNode<?> newNode) { // mark the node as moved if node has been removed ASTNode removedASTNode = newNode.getRemovedASTNode(); if (removedASTNode != null) { nodeToBeMoved(removedASTNode); } newNode.setParent(this); newNode.ancestorInserted(); } /** * Get the original contents of the node using the source. * <p> * The contents of the node in AST implementation includes only the node itself without * the leading or trailing whitespace. * If the node has a Javadoc comment, it is included in the contents. No other leading or trailing * comments are included in the node contents. * <p> * Note that this method returns the contents before any modifications. This method will not * return the correct contents if the node has been cloned, removed or moved. * * @see org.eclipse.emf.codegen.merge.java.facade.JNode#getContents() */ public String getContents() { return facadeHelper.toString(getASTNode()); } /** * @return the removedASTNode */ protected ASTNode getRemovedASTNode() { return removedASTNode; } /** * Inserts AST node wrapped by new node beside target node in the list defined by given property. * <p> * No checks are performed if the new node can be inserted nor if the target node exists. * * @param newNode * @param property * @param targetNode * @param before */ protected void insert(ASTJNode<?> newNode, ChildListPropertyDescriptor property, ASTJNode<?> targetNode, boolean before) { ListRewrite listRewrite = rewriter.getListRewrite(getASTNode(), property); if (before) { listRewrite.insertBefore(newNode.getWrappedObject(), targetNode.getWrappedObject(), null); } else { listRewrite.insertAfter(newNode.getWrappedObject(), targetNode.getWrappedObject(), null); } finishInsert(newNode); } /** * Inserts AST node wrapped by new node at the beginning of the list of nodes defined by given property. * <p> * No checks are performed if the new node can be inserted nor if the target node exists. * * @param newNode * @param property */ protected void insertFirst(ASTJNode<?> newNode, ChildListPropertyDescriptor property) { ListRewrite listRewrite = rewriter.getListRewrite(getASTNode(), property); listRewrite.insertFirst(newNode.getWrappedObject(), null); finishInsert(newNode); } /** * Inserts AST node wrapped by new node at the end of the list of nodes defined by given property. * <p> * No checks are performed if the new node can be inserted nor if the target node exists. * * @param newNode * @param property */ protected void insertLast(ASTJNode<?> newNode, ChildListPropertyDescriptor property) { ListRewrite listRewrite = rewriter.getListRewrite(getASTNode(), property); listRewrite.insertLast(newNode.getWrappedObject(), null); finishInsert(newNode); } /** * Inserts <code>newSibling</code> as a child of this node before or after the given <code>node</code>. * <p> * Default implementation does nothing and returns <code>false</code>. * * @param node * @param newSibling to insert * @param before <code>true</code> if <code>newSibling</code> must be before <code>node</code>, <code>false</code> if after * @return <code>true</code> if operation successful, <code>false</code> otherwise */ public boolean insertSibling(ASTJNode<?> node, ASTJNode<?> newSibling, boolean before) { return false; } /** * Notifies the range computer of {@link ASTRewrite} that the node will be moved. * * @param node to be moved * * @see CommentAwareSourceRangeComputer#computeSourceRange(ASTNode) */ protected void nodeToBeMoved(ASTNode node) { TargetSourceRangeComputer sourceRangeComputer = rewriter.getExtendedSourceRangeComputer(); if (sourceRangeComputer instanceof CommentAwareSourceRangeComputer) { ((CommentAwareSourceRangeComputer)sourceRangeComputer).unmarkNodeForRemoval(node); } } /** * Notifies the range computer of {@link ASTRewrite} that the node will be removed. * * @param node to be removed * @see CommentAwareSourceRangeComputer#computeSourceRange(ASTNode) */ protected void nodeToBeRemoved(ASTNode node) { TargetSourceRangeComputer sourceRangeComputer = rewriter.getExtendedSourceRangeComputer(); if (sourceRangeComputer instanceof CommentAwareSourceRangeComputer) { ((CommentAwareSourceRangeComputer)sourceRangeComputer).markNodeForRemoval(node); } } /** * Removes a node. * <p> * Default implementation does nothing and returns <code>false</code>. * * @param node must be a child of this node * @return <code>true</code> if operation successful, <code>false</code> otherwise */ public boolean remove(ASTJNode<?> node) { return false; } /** * Removes AST node wrapped by given <code>ASTJNode</code> from the given property. * <p> * This method notifies children and source range computer that the node is removed, then creates * a place-holder node that allows to insert the removed node later. * <p> * No checks are performed if the node is a child of this node. * * @param node * @param property */ protected void remove(ASTJNode<?> node, ChildListPropertyDescriptor property) { node.ancestorToBeRemoved(); ASTNode astNodeToBeRemoved = node.getWrappedObject(); // if are dealing with original, not cloned node if (astNodeToBeRemoved.getParent() != null && astNodeToBeRemoved.getLocationInParent() != null) { // mark the node to be removed nodeToBeRemoved(astNodeToBeRemoved); // assume that the node is being moved (to allow insertion after) node.setRemovedASTNode(astNodeToBeRemoved); ASTNode moveTargetASTNode = rewriter.createMoveTarget(astNodeToBeRemoved); node.setWrappedObject(moveTargetASTNode); getFacadeHelper().updateObjectToNodeMap(node); } // remove the node removeNodeFromListProperty(getASTNode(), astNodeToBeRemoved, property); node.setParent(null); } /** * Removes value from list property. * * @param parentNode * @param nodeToRemove * @param property */ protected void removeNodeFromListProperty(ASTNode parentNode, ASTNode nodeToRemove, ChildListPropertyDescriptor property) { if (ASTFacadeHelper.DEBUG) { facadeHelper.logInfo("Removing node from list property <" + nodeToRemove + ">"); } ListRewrite listRewrite = getRewriter().getListRewrite(parentNode, property); listRewrite.remove(nodeToRemove, null); } /** * Permanently disables tracking for the given AST node. * <p> * The given <code>node</code> will be modified only by {@link ASTRewrite}. * * @param node to disable tracking for * * @see #disableTrackAndReplace() * @see #trackAndReplace(ASTNode, String) */ protected void removeTrackAndReplace(ASTNode node) { trackedContentsMap.remove(node); ASTJCompilationUnit compilationUnit = (ASTJCompilationUnit)facadeHelper.getCompilationUnit(this); if (compilationUnit != null) { compilationUnit.getAllTrackedContentsMap().remove(node); } // TODO handle situation when ASTJNode is removed (compilationUnit is null) } /** * Default implementation does nothing. * * @see org.eclipse.emf.codegen.merge.java.facade.JNode#setFlags(int) */ public void setFlags(int flags) { // default implementation does nothing } /** * Sets the list property to the given array of values. For each string in the array, * a string place-holder node is created. * * @param node * @param values * @param property * @param nodeType of the string place holders to create */ protected void setListNodeProperty(ASTNode node, String[] values, ChildListPropertyDescriptor property, int nodeType) { ListRewrite listRewrite = rewriter.getListRewrite(node, property); @SuppressWarnings("unchecked") List<ASTNode> oldValues = listRewrite.getRewrittenList(); // clear old values for (ASTNode oldValue : oldValues) { listRewrite.remove(oldValue, null); } // insert new values for (String value : values) { listRewrite.insertLast(rewriter.createStringPlaceholder(value, nodeType), null); } } /** * Sets the property of the node to the given value. * <p> * Note that the type <code>value</code> parameter will only be checked by * {@link ASTRewrite#set(ASTNode, StructuralPropertyDescriptor, Object, org.eclipse.text.edits.TextEditGroup)} * <p> * * @param node * @param value * @param property */ protected void setNodeProperty(ASTNode node, Object value, StructuralPropertyDescriptor property) { if (ASTFacadeHelper.DEBUG) { facadeHelper.logInfo("Setting node property <" + property + "> to <" + value + ">"); } rewriter.set(node, property, value, null); } /** * Sets the property of the node to the given string value. * <p> * Note that the type <code>value</code> parameter will only be checked by * {@link ASTRewrite#set(ASTNode, StructuralPropertyDescriptor, Object, org.eclipse.text.edits.TextEditGroup)} * <p> * * @param node * @param stringValue * @param property * @param nodeType */ protected void setNodeProperty(ASTNode node, String stringValue, StructuralPropertyDescriptor property, int nodeType) { if (ASTFacadeHelper.DEBUG) { facadeHelper.logInfo("Setting node property to <" + stringValue + ">"); } Object value = null; if (stringValue != null) { value = rewriter.createStringPlaceholder(stringValue, nodeType); } rewriter.set(node, property, value, null); } /** * Adds strings from strings list to existing array of strings, returns resulting * string array, and clears the list of strings. * * @param strings can <b>not</b> be <code>null</code> * @param stringsList can be <code>null</code> */ protected String[] combineArrayAndList(String[] strings, List<String> stringsList) { if (stringsList != null && stringsList.size() > 0) { if (strings.length > 0) { String[] newStringArray = new String[strings.length + stringsList.size()]; for (int i = 0; i < strings.length; i++) { newStringArray[i] = strings[i]; } for (int i = 0; i < stringsList.size(); i++) { newStringArray[strings.length + i] = stringsList.get(i); } strings = newStringArray; } else { strings = stringsList.toArray(EMPTY_STRING_ARRAY); } stringsList.clear(); } return strings; } /** * @param removedASTNode the removedASTNode to set */ protected void setRemovedASTNode(ASTNode removedASTNode) { this.removedASTNode = removedASTNode; } /** * Sets the property of the node. The position of the property value is tracked and value * is replaced by <code>stringValue</code> during rewrite process. * * @param node to set property of * @param stringValue * @param property must be {@link SimplePropertyDescriptor} or {@link ChildPropertyDescriptor} * @param nodeType to use if value is null, and new node has to be created */ protected void setTrackedNodeProperty(ASTNode node, String stringValue, StructuralPropertyDescriptor property, int nodeType) { if (ASTFacadeHelper.DEBUG) { facadeHelper.logInfo("Setting tracked node property to string <" + stringValue + ">"); } ASTNode nodeValue = (ASTNode)rewriter.get(node, property); if (stringValue == null || "".equals(stringValue)) { removeTrackAndReplace(nodeValue); rewriter.set(node, property, null, null); } else if (nodeValue != null) { trackAndReplace(nodeValue, stringValue); } else // stringValue not null, nodeValue is null { nodeValue = rewriter.createStringPlaceholder(stringValue, nodeType); rewriter.set(node, property, nodeValue, null); trackAndReplace(nodeValue, stringValue); } } /** * Records that the given <code>node</code> must have the given <code>contents</code>. * <p> * During rewrite process, the position of the <code>node</code> will be tracked, and the node * will replaced by the given <code>contents</code>. No changes will be made to <code>contents</code>. * <p> * This method should be used to undo correction of indentation done by {@link ASTRewrite#rewriteAST(org.eclipse.jface.text.IDocument, Map)} * on string place-holder nodes ({@link ASTRewrite#createStringPlaceholder(String, int)}) * during rewrite process. * <p> * Note that all the tracked nodes must exist in the rewritten tree. If some tracked nodes are removed from rewritten tree, wrong * contents may be replaced. Use {@link #disableTrackAndReplace()} when <code>ASTJNode</code>s are removed. * * @param node * @param contents */ protected void trackAndReplace(ASTNode node, String contents) { if (ASTFacadeHelper.DEBUG) { facadeHelper.logInfo("Track and replace <" + node + "> by <" + contents + ">"); } trackedContentsMap.put(node, contents); ASTJCompilationUnit compilationUnit = ((ASTJCompilationUnit)facadeHelper.getCompilationUnit(this)); if (compilationUnit != null) { compilationUnit.getAllTrackedContentsMap().put(node, contents); } } /** * Comments out this node. Node is not removed from the tree, and is still returned by getChildren(). */ public void commentOut() { isCommentedOut = true; ASTJCompilationUnit compilationUnit = ((ASTJCompilationUnit)facadeHelper.getCompilationUnit(this)); if (compilationUnit != null) { compilationUnit.getCommentedOutNodes().add(this.getASTNode()); } } }