/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.refactoring; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; import static com.android.SdkConstants.ATTR_TEXT; import static com.android.SdkConstants.EXT_XML; import static com.android.SdkConstants.VIEW_FRAGMENT; import static com.android.SdkConstants.VIEW_INCLUDE; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.viewers.ITreeSelection; import org.eclipse.ltk.core.refactoring.Change; import org.eclipse.ltk.core.refactoring.Refactoring; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.ltk.core.refactoring.TextFileChange; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import org.eclipse.wst.xml.core.internal.document.ElementImpl; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Changes the type of the given widgets to the given target type * and updates the attributes if necessary */ @SuppressWarnings("restriction") // XML model public class ChangeViewRefactoring extends VisualRefactoring { private static final String KEY_TYPE = "type"; //$NON-NLS-1$ private String mTypeFqcn; /** * This constructor is solely used by {@link Descriptor}, * to replay a previous refactoring. * @param arguments argument map created by #createArgumentMap. */ ChangeViewRefactoring(Map<String, String> arguments) { super(arguments); mTypeFqcn = arguments.get(KEY_TYPE); } public ChangeViewRefactoring( IFile file, LayoutEditorDelegate delegate, ITextSelection selection, ITreeSelection treeSelection) { super(file, delegate, selection, treeSelection); } @VisibleForTesting ChangeViewRefactoring(List<Element> selectedElements, LayoutEditorDelegate editor) { super(selectedElements, editor); } @Override public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException { RefactoringStatus status = new RefactoringStatus(); try { pm.beginTask("Checking preconditions...", 6); if (mSelectionStart == -1 || mSelectionEnd == -1) { status.addFatalError("No selection to convert"); return status; } // Make sure the selection is contiguous if (mTreeSelection != null) { List<CanvasViewInfo> infos = getSelectedViewInfos(); if (!validateNotEmpty(infos, status)) { return status; } } // Ensures that we have a valid DOM model: if (mElements.size() == 0) { status.addFatalError("Nothing to convert"); return status; } pm.worked(1); return status; } finally { pm.done(); } } @Override protected VisualRefactoringDescriptor createDescriptor() { String comment = getName(); return new Descriptor( mProject.getName(), //project comment, //description comment, //comment createArgumentMap()); } @Override protected Map<String, String> createArgumentMap() { Map<String, String> args = super.createArgumentMap(); args.put(KEY_TYPE, mTypeFqcn); return args; } @Override public String getName() { return "Change Widget Type"; } void setType(String typeFqcn) { mTypeFqcn = typeFqcn; } @Override protected @NonNull List<Change> computeChanges(IProgressMonitor monitor) { String name = getViewClass(mTypeFqcn); IFile file = mDelegate.getEditor().getInputFile(); List<Change> changes = new ArrayList<Change>(); if (file == null) { return changes; } TextFileChange change = new TextFileChange(file.getName(), file); MultiTextEdit rootEdit = new MultiTextEdit(); change.setEdit(rootEdit); change.setTextType(EXT_XML); changes.add(change); for (Element element : getElements()) { IndexedRegion region = getRegion(element); String text = getText(region.getStartOffset(), region.getEndOffset()); String oldName = element.getNodeName(); int open = text.indexOf(oldName); int close = text.lastIndexOf(oldName); if (element instanceof ElementImpl && ((ElementImpl) element).isEmptyTag()) { close = -1; } if (open != -1) { int oldLength = oldName.length(); rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + open, oldLength, name)); } if (close != -1 && close != open) { int oldLength = oldName.length(); rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + close, oldLength, name)); } // Change tag type String oldId = getId(element); String newId = ensureIdMatchesType(element, mTypeFqcn, rootEdit); // Update any layout references to the old id with the new id if (oldId != null && newId != null) { IStructuredModel model = mDelegate.getEditor().getModelForRead(); try { IStructuredDocument doc = model.getStructuredDocument(); if (doc != null) { IndexedRegion range = getRegion(element); int skipStart = range.getStartOffset(); int skipEnd = range.getEndOffset(); List<TextEdit> replaceIds = replaceIds(getAndroidNamespacePrefix(), doc, skipStart, skipEnd, oldId, newId); for (TextEdit edit : replaceIds) { rootEdit.addChild(edit); } } } finally { model.releaseFromRead(); } } // Strip out attributes that no longer make sense removeUndefinedAttrs(rootEdit, element); } return changes; } /** Removes all the unused attributes after a conversion */ private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element element) { ViewElementDescriptor descriptor = getElementDescriptor(mTypeFqcn); if (descriptor == null) { return; } Set<String> defined = new HashSet<String>(); AttributeDescriptor[] layoutAttributes = descriptor.getAttributes(); for (AttributeDescriptor attribute : layoutAttributes) { defined.add(attribute.getXmlLocalName()); } List<Attr> attributes = findAttributes(element); for (Attr attribute : attributes) { String name = attribute.getLocalName(); if (!defined.contains(name)) { // Remove it removeAttribute(rootEdit, element, attribute.getNamespaceURI(), name); } } // Set text attribute if it's defined if (defined.contains(ATTR_TEXT) && !element.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) { setAttribute(rootEdit, element, ANDROID_URI, getAndroidNamespacePrefix(), ATTR_TEXT, descriptor.getUiName()); } } protected List<Attr> findAttributes(Node root) { List<Attr> result = new ArrayList<Attr>(); NamedNodeMap attributes = root.getAttributes(); for (int i = 0, n = attributes.getLength(); i < n; i++) { Node attributeNode = attributes.item(i); String name = attributeNode.getLocalName(); if (!name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) && ANDROID_URI.equals(attributeNode.getNamespaceURI())) { result.add((Attr) attributeNode); } } return result; } List<String> getOldTypes() { List<String> types = new ArrayList<String>(); for (Element primary : getElements()) { String oldType = primary.getTagName(); if (oldType.indexOf('.') == -1 && !oldType.equals(VIEW_INCLUDE) && !oldType.equals(VIEW_FRAGMENT)) { oldType = ANDROID_WIDGET_PREFIX + oldType; } types.add(oldType); } return types; } @Override VisualRefactoringWizard createWizard() { return new ChangeViewWizard(this, mDelegate); } public static class Descriptor extends VisualRefactoringDescriptor { public Descriptor(String project, String description, String comment, Map<String, String> arguments) { super("com.android.ide.eclipse.adt.refactoring.changeview", //$NON-NLS-1$ project, description, comment, arguments); } @Override protected Refactoring createRefactoring(Map<String, String> args) { return new ChangeViewRefactoring(args); } } }