1/*
2 * Copyright (C) 2010 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
17package com.android.ide.eclipse.adt.internal.refactorings.core;
18
19import static com.android.SdkConstants.ANDROID_URI;
20import static com.android.SdkConstants.ATTR_CLASS;
21import static com.android.SdkConstants.ATTR_CONTEXT;
22import static com.android.SdkConstants.ATTR_NAME;
23import static com.android.SdkConstants.DOT_XML;
24import static com.android.SdkConstants.EXT_XML;
25import static com.android.SdkConstants.TOOLS_URI;
26import static com.android.SdkConstants.VIEW_FRAGMENT;
27import static com.android.SdkConstants.VIEW_TAG;
28
29import com.android.SdkConstants;
30import com.android.annotations.NonNull;
31import com.android.ide.common.xml.ManifestData;
32import com.android.ide.eclipse.adt.AdtConstants;
33import com.android.ide.eclipse.adt.AdtPlugin;
34import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
35import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
36import com.android.ide.eclipse.adt.internal.sdk.Sdk;
37import com.android.resources.ResourceFolderType;
38import com.android.utils.SdkUtils;
39
40import org.eclipse.core.resources.IFile;
41import org.eclipse.core.resources.IFolder;
42import org.eclipse.core.resources.IProject;
43import org.eclipse.core.resources.IResource;
44import org.eclipse.core.runtime.CoreException;
45import org.eclipse.core.runtime.IProgressMonitor;
46import org.eclipse.core.runtime.OperationCanceledException;
47import org.eclipse.jdt.core.IJavaElement;
48import org.eclipse.jdt.core.IJavaProject;
49import org.eclipse.jdt.core.IPackageFragment;
50import org.eclipse.jdt.core.IType;
51import org.eclipse.jdt.core.JavaModelException;
52import org.eclipse.ltk.core.refactoring.Change;
53import org.eclipse.ltk.core.refactoring.CompositeChange;
54import org.eclipse.ltk.core.refactoring.RefactoringStatus;
55import org.eclipse.ltk.core.refactoring.TextFileChange;
56import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
57import org.eclipse.ltk.core.refactoring.participants.MoveParticipant;
58import org.eclipse.text.edits.MultiTextEdit;
59import org.eclipse.text.edits.ReplaceEdit;
60import org.eclipse.text.edits.TextEdit;
61import org.eclipse.wst.sse.core.StructuredModelManager;
62import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
63import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
64import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
65import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
66import org.w3c.dom.Attr;
67import org.w3c.dom.Element;
68import org.w3c.dom.NamedNodeMap;
69import org.w3c.dom.Node;
70import org.w3c.dom.NodeList;
71
72import java.io.IOException;
73import java.util.ArrayList;
74import java.util.Collection;
75import java.util.List;
76
77/**
78 * A participant to participate in refactorings that move a type in an Android project.
79 * The class updates android manifest and the layout file
80 * The user can suppress refactoring by disabling the "Update references" checkbox
81 * <p>
82 * Rename participants are registered via the extension point <code>
83 * org.eclipse.ltk.core.refactoring.moveParticipants</code>.
84 * Extensions to this extension point must therefore extend <code>org.eclipse.ltk.core.refactoring.participants.MoveParticipant</code>.
85 * </p>
86 */
87@SuppressWarnings("restriction")
88public class AndroidTypeMoveParticipant extends MoveParticipant {
89
90    private IProject mProject;
91    protected IFile mManifestFile;
92    protected String mOldFqcn;
93    protected String mNewFqcn;
94    protected String mAppPackage;
95
96    @Override
97    public String getName() {
98        return "Android Type Move";
99    }
100
101    @Override
102    public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
103            throws OperationCanceledException {
104        return new RefactoringStatus();
105    }
106
107    @Override
108    protected boolean initialize(Object element) {
109        if (element instanceof IType) {
110            IType type = (IType) element;
111            IJavaProject javaProject = (IJavaProject) type.getAncestor(IJavaElement.JAVA_PROJECT);
112            mProject = javaProject.getProject();
113            IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP
114                    + SdkConstants.FN_ANDROID_MANIFEST_XML);
115
116            if (manifestResource == null || !manifestResource.exists()
117                    || !(manifestResource instanceof IFile)) {
118                RefactoringUtil.logInfo("Invalid or missing the "
119                        + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the " + mProject.getName()
120                        + " project.");
121                return false;
122            }
123            mManifestFile = (IFile) manifestResource;
124            ManifestData manifestData;
125            manifestData = AndroidManifestHelper.parseForData(mManifestFile);
126            if (manifestData == null) {
127                return false;
128            }
129            mAppPackage = manifestData.getPackage();
130            mOldFqcn = type.getFullyQualifiedName();
131            Object destination = getArguments().getDestination();
132            if (destination instanceof IPackageFragment) {
133                IPackageFragment packageFragment = (IPackageFragment) destination;
134                mNewFqcn = packageFragment.getElementName() + "." + type.getElementName();
135            } else if (destination instanceof IResource) {
136                try {
137                    IPackageFragment[] fragments = javaProject.getPackageFragments();
138                    for (IPackageFragment fragment : fragments) {
139                        IResource resource = fragment.getResource();
140                        if (resource.equals(destination)) {
141                            mNewFqcn = fragment.getElementName() + '.' + type.getElementName();
142                            break;
143                        }
144                    }
145                } catch (JavaModelException e) {
146                    // pass
147                }
148            }
149            return mOldFqcn != null && mNewFqcn != null;
150        }
151
152        return false;
153    }
154
155    @Override
156    public Change createChange(IProgressMonitor pm) throws CoreException,
157            OperationCanceledException {
158        if (pm.isCanceled()) {
159            return null;
160        }
161        if (!getArguments().getUpdateReferences()) {
162            return null;
163        }
164        CompositeChange result = new CompositeChange(getName());
165        result.markAsSynthetic();
166
167        addManifestFileChanges(result);
168
169        // Update layout files; we don't just need to react to custom view
170        // changes, we need to update fragment references and even tool:context activity
171        // references
172        addLayoutFileChanges(mProject, result);
173
174        // Also update in dependent projects
175        ProjectState projectState = Sdk.getProjectState(mProject);
176        if (projectState != null) {
177            Collection<ProjectState> parentProjects = projectState.getFullParentProjects();
178            for (ProjectState parentProject : parentProjects) {
179                IProject project = parentProject.getProject();
180                addLayoutFileChanges(project, result);
181            }
182        }
183
184        return (result.getChildren().length == 0) ? null : result;
185    }
186
187    private void addManifestFileChanges(CompositeChange result) {
188        addXmlFileChanges(mManifestFile, result, true);
189    }
190
191    private void addLayoutFileChanges(IProject project, CompositeChange result) {
192        try {
193            // Update references in XML resource files
194            IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
195
196            IResource[] folders = resFolder.members();
197            for (IResource folder : folders) {
198                String folderName = folder.getName();
199                ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
200                if (folderType != ResourceFolderType.LAYOUT) {
201                    continue;
202                }
203                if (!(folder instanceof IFolder)) {
204                    continue;
205                }
206                IResource[] files = ((IFolder) folder).members();
207                for (int i = 0; i < files.length; i++) {
208                    IResource member = files[i];
209                    if ((member instanceof IFile) && member.exists()) {
210                        IFile file = (IFile) member;
211                        String fileName = member.getName();
212
213                        if (SdkUtils.endsWith(fileName, DOT_XML)) {
214                            addXmlFileChanges(file, result, false);
215                        }
216                    }
217                }
218            }
219        } catch (CoreException e) {
220            RefactoringUtil.log(e);
221        }
222    }
223
224    private boolean addXmlFileChanges(IFile file, CompositeChange changes, boolean isManifest) {
225        IModelManager modelManager = StructuredModelManager.getModelManager();
226        IStructuredModel model = null;
227        try {
228            model = modelManager.getExistingModelForRead(file);
229            if (model == null) {
230                model = modelManager.getModelForRead(file);
231            }
232            if (model != null) {
233                IStructuredDocument document = model.getStructuredDocument();
234                if (model instanceof IDOMModel) {
235                    IDOMModel domModel = (IDOMModel) model;
236                    Element root = domModel.getDocument().getDocumentElement();
237                    if (root != null) {
238                        List<TextEdit> edits = new ArrayList<TextEdit>();
239                        if (isManifest) {
240                            addManifestReplacements(edits, root, document);
241                        } else {
242                            addLayoutReplacements(edits, root, document);
243                        }
244                        if (!edits.isEmpty()) {
245                            MultiTextEdit rootEdit = new MultiTextEdit();
246                            rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
247                            TextFileChange change = new TextFileChange(file.getName(), file);
248                            change.setTextType(EXT_XML);
249                            change.setEdit(rootEdit);
250                            changes.add(change);
251                        }
252                    }
253                } else {
254                    return false;
255                }
256            }
257
258            return true;
259        } catch (IOException e) {
260            AdtPlugin.log(e, null);
261        } catch (CoreException e) {
262            AdtPlugin.log(e, null);
263        } finally {
264            if (model != null) {
265                model.releaseFromRead();
266            }
267        }
268
269        return false;
270    }
271
272    private void addLayoutReplacements(
273            @NonNull List<TextEdit> edits,
274            @NonNull Element element,
275            @NonNull IStructuredDocument document) {
276        String tag = element.getTagName();
277        if (tag.equals(mOldFqcn)) {
278            int start = RefactoringUtil.getTagNameRangeStart(element, document);
279            if (start != -1) {
280                int end = start + mOldFqcn.length();
281                edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
282            }
283        } else if (tag.equals(VIEW_TAG)) {
284            Attr classNode = element.getAttributeNode(ATTR_CLASS);
285            if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
286                int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
287                if (start != -1) {
288                    int end = start + mOldFqcn.length();
289                    edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
290                }
291            }
292        } else if (tag.equals(VIEW_FRAGMENT)) {
293            Attr classNode = element.getAttributeNode(ATTR_CLASS);
294            if (classNode == null) {
295                classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
296            }
297            if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
298                int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
299                if (start != -1) {
300                    int end = start + mOldFqcn.length();
301                    edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
302                }
303            }
304        } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) {
305            Attr classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT);
306            if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
307                int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
308                if (start != -1) {
309                    int end = start + mOldFqcn.length();
310                    edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
311                }
312            }
313        }
314
315        NodeList children = element.getChildNodes();
316        for (int i = 0, n = children.getLength(); i < n; i++) {
317            Node child = children.item(i);
318            if (child.getNodeType() == Node.ELEMENT_NODE) {
319                addLayoutReplacements(edits, (Element) child, document);
320            }
321        }
322    }
323
324    private void addManifestReplacements(
325            @NonNull List<TextEdit> edits,
326            @NonNull Element element,
327            @NonNull IStructuredDocument document) {
328        NamedNodeMap attributes = element.getAttributes();
329        for (int i = 0, n = attributes.getLength(); i < n; i++) {
330            Attr attr = (Attr) attributes.item(i);
331            if (!RefactoringUtil.isManifestClassAttribute(attr)) {
332                continue;
333            }
334
335            String value = attr.getValue();
336            if (value.equals(mOldFqcn)) {
337                int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
338                if (start != -1) {
339                    int end = start + mOldFqcn.length();
340                    edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
341                }
342            } else if (value.startsWith(".")) { //$NON-NLS-1$
343                String fqcn = mAppPackage + value;
344                if (fqcn.equals(mOldFqcn)) {
345                    int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
346                    if (start != -1) {
347                        int end = start + value.length();
348                        edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
349                    }
350                }
351            }
352        }
353
354        NodeList children = element.getChildNodes();
355        for (int i = 0, n = children.getLength(); i < n; i++) {
356            Node child = children.item(i);
357            if (child.getNodeType() == Node.ELEMENT_NODE) {
358                addManifestReplacements(edits, (Element) child, document);
359            }
360        }
361    }
362}
363