1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring; 17 18 import static com.android.SdkConstants.ANDROID_URI; 19 import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; 20 import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; 21 import static com.android.SdkConstants.ATTR_TEXT; 22 import static com.android.SdkConstants.EXT_XML; 23 import static com.android.SdkConstants.VIEW_FRAGMENT; 24 import static com.android.SdkConstants.VIEW_INCLUDE; 25 26 import com.android.annotations.NonNull; 27 import com.android.annotations.VisibleForTesting; 28 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; 29 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 30 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; 32 33 import org.eclipse.core.resources.IFile; 34 import org.eclipse.core.runtime.CoreException; 35 import org.eclipse.core.runtime.IProgressMonitor; 36 import org.eclipse.core.runtime.OperationCanceledException; 37 import org.eclipse.jface.text.ITextSelection; 38 import org.eclipse.jface.viewers.ITreeSelection; 39 import org.eclipse.ltk.core.refactoring.Change; 40 import org.eclipse.ltk.core.refactoring.Refactoring; 41 import org.eclipse.ltk.core.refactoring.RefactoringStatus; 42 import org.eclipse.ltk.core.refactoring.TextFileChange; 43 import org.eclipse.text.edits.MultiTextEdit; 44 import org.eclipse.text.edits.ReplaceEdit; 45 import org.eclipse.text.edits.TextEdit; 46 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 47 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 48 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 49 import org.eclipse.wst.xml.core.internal.document.ElementImpl; 50 import org.w3c.dom.Attr; 51 import org.w3c.dom.Element; 52 import org.w3c.dom.NamedNodeMap; 53 import org.w3c.dom.Node; 54 55 import java.util.ArrayList; 56 import java.util.HashSet; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.Set; 60 61 /** 62 * Changes the type of the given widgets to the given target type 63 * and updates the attributes if necessary 64 */ 65 @SuppressWarnings("restriction") // XML model 66 public class ChangeViewRefactoring extends VisualRefactoring { 67 private static final String KEY_TYPE = "type"; //$NON-NLS-1$ 68 private String mTypeFqcn; 69 70 /** 71 * This constructor is solely used by {@link Descriptor}, 72 * to replay a previous refactoring. 73 * @param arguments argument map created by #createArgumentMap. 74 */ 75 ChangeViewRefactoring(Map<String, String> arguments) { 76 super(arguments); 77 mTypeFqcn = arguments.get(KEY_TYPE); 78 } 79 80 public ChangeViewRefactoring( 81 IFile file, 82 LayoutEditorDelegate delegate, 83 ITextSelection selection, 84 ITreeSelection treeSelection) { 85 super(file, delegate, selection, treeSelection); 86 } 87 88 @VisibleForTesting 89 ChangeViewRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) { 90 super(selectedElements, editor); 91 } 92 93 @Override 94 public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, 95 OperationCanceledException { 96 RefactoringStatus status = new RefactoringStatus(); 97 98 try { 99 pm.beginTask("Checking preconditions...", 6); 100 101 if (mSelectionStart == -1 || mSelectionEnd == -1) { 102 status.addFatalError("No selection to convert"); 103 return status; 104 } 105 106 // Make sure the selection is contiguous 107 if (mTreeSelection != null) { 108 List<CanvasViewInfo> infos = getSelectedViewInfos(); 109 if (!validateNotEmpty(infos, status)) { 110 return status; 111 } 112 } 113 114 // Ensures that we have a valid DOM model: 115 if (mElements.size() == 0) { 116 status.addFatalError("Nothing to convert"); 117 return status; 118 } 119 120 pm.worked(1); 121 return status; 122 123 } finally { 124 pm.done(); 125 } 126 } 127 128 @Override 129 protected VisualRefactoringDescriptor createDescriptor() { 130 String comment = getName(); 131 return new Descriptor( 132 mProject.getName(), //project 133 comment, //description 134 comment, //comment 135 createArgumentMap()); 136 } 137 138 @Override 139 protected Map<String, String> createArgumentMap() { 140 Map<String, String> args = super.createArgumentMap(); 141 args.put(KEY_TYPE, mTypeFqcn); 142 143 return args; 144 } 145 146 @Override 147 public String getName() { 148 return "Change Widget Type"; 149 } 150 151 void setType(String typeFqcn) { 152 mTypeFqcn = typeFqcn; 153 } 154 155 @Override 156 protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) { 157 String name = getViewClass(mTypeFqcn); 158 159 IFile file = mDelegate.getEditor().getInputFile(); 160 List<Change> changes = new ArrayList<Change>(); 161 if (file == null) { 162 return changes; 163 } 164 TextFileChange change = new TextFileChange(file.getName(), file); 165 MultiTextEdit rootEdit = new MultiTextEdit(); 166 change.setEdit(rootEdit); 167 change.setTextType(EXT_XML); 168 changes.add(change); 169 170 for (Element element : getElements()) { 171 IndexedRegion region = getRegion(element); 172 String text = getText(region.getStartOffset(), region.getEndOffset()); 173 String oldName = element.getNodeName(); 174 int open = text.indexOf(oldName); 175 int close = text.lastIndexOf(oldName); 176 if (element instanceof ElementImpl && ((ElementImpl) element).isEmptyTag()) { 177 close = -1; 178 } 179 180 if (open != -1) { 181 int oldLength = oldName.length(); 182 rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + open, 183 oldLength, name)); 184 } 185 if (close != -1 && close != open) { 186 int oldLength = oldName.length(); 187 rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + close, oldLength, 188 name)); 189 } 190 191 // Change tag type 192 String oldId = getId(element); 193 String newId = ensureIdMatchesType(element, mTypeFqcn, rootEdit); 194 // Update any layout references to the old id with the new id 195 if (oldId != null && newId != null) { 196 IStructuredModel model = mDelegate.getEditor().getModelForRead(); 197 try { 198 IStructuredDocument doc = model.getStructuredDocument(); 199 if (doc != null) { 200 IndexedRegion range = getRegion(element); 201 int skipStart = range.getStartOffset(); 202 int skipEnd = range.getEndOffset(); 203 List<TextEdit> replaceIds = replaceIds(getAndroidNamespacePrefix(), doc, 204 skipStart, skipEnd, 205 oldId, newId); 206 for (TextEdit edit : replaceIds) { 207 rootEdit.addChild(edit); 208 } 209 } 210 } finally { 211 model.releaseFromRead(); 212 } 213 } 214 215 // Strip out attributes that no longer make sense 216 removeUndefinedAttrs(rootEdit, element); 217 } 218 219 return changes; 220 } 221 222 /** Removes all the unused attributes after a conversion */ 223 private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element element) { 224 ViewElementDescriptor descriptor = getElementDescriptor(mTypeFqcn); 225 if (descriptor == null) { 226 return; 227 } 228 229 Set<String> defined = new HashSet<String>(); 230 AttributeDescriptor[] layoutAttributes = descriptor.getAttributes(); 231 for (AttributeDescriptor attribute : layoutAttributes) { 232 defined.add(attribute.getXmlLocalName()); 233 } 234 235 List<Attr> attributes = findAttributes(element); 236 for (Attr attribute : attributes) { 237 String name = attribute.getLocalName(); 238 if (!defined.contains(name)) { 239 // Remove it 240 removeAttribute(rootEdit, element, attribute.getNamespaceURI(), name); 241 } 242 } 243 244 // Set text attribute if it's defined 245 if (defined.contains(ATTR_TEXT) && !element.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) { 246 setAttribute(rootEdit, element, ANDROID_URI, getAndroidNamespacePrefix(), 247 ATTR_TEXT, descriptor.getUiName()); 248 } 249 } 250 251 protected List<Attr> findAttributes(Node root) { 252 List<Attr> result = new ArrayList<Attr>(); 253 NamedNodeMap attributes = root.getAttributes(); 254 for (int i = 0, n = attributes.getLength(); i < n; i++) { 255 Node attributeNode = attributes.item(i); 256 257 String name = attributeNode.getLocalName(); 258 if (!name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) 259 && ANDROID_URI.equals(attributeNode.getNamespaceURI())) { 260 result.add((Attr) attributeNode); 261 } 262 } 263 264 return result; 265 } 266 267 List<String> getOldTypes() { 268 List<String> types = new ArrayList<String>(); 269 for (Element primary : getElements()) { 270 String oldType = primary.getTagName(); 271 if (oldType.indexOf('.') == -1 272 && !oldType.equals(VIEW_INCLUDE) && !oldType.equals(VIEW_FRAGMENT)) { 273 oldType = ANDROID_WIDGET_PREFIX + oldType; 274 } 275 types.add(oldType); 276 } 277 278 return types; 279 } 280 281 @Override 282 VisualRefactoringWizard createWizard() { 283 return new ChangeViewWizard(this, mDelegate); 284 } 285 286 public static class Descriptor extends VisualRefactoringDescriptor { 287 public Descriptor(String project, String description, String comment, 288 Map<String, String> arguments) { 289 super("com.android.ide.eclipse.adt.refactoring.changeview", //$NON-NLS-1$ 290 project, description, comment, arguments); 291 } 292 293 @Override 294 protected Refactoring createRefactoring(Map<String, String> args) { 295 return new ChangeViewRefactoring(args); 296 } 297 } 298 } 299