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 
17 package com.android.ide.eclipse.adt.internal.refactorings.core;
18 
19 import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
20 import static com.android.SdkConstants.ANDROID_URI;
21 import static com.android.SdkConstants.ATTR_CLASS;
22 import static com.android.SdkConstants.ATTR_CONTEXT;
23 import static com.android.SdkConstants.ATTR_NAME;
24 import static com.android.SdkConstants.CLASS_VIEW;
25 import static com.android.SdkConstants.DOT_XML;
26 import static com.android.SdkConstants.EXT_XML;
27 import static com.android.SdkConstants.R_CLASS;
28 import static com.android.SdkConstants.TOOLS_URI;
29 import static com.android.SdkConstants.VIEW_FRAGMENT;
30 import static com.android.SdkConstants.VIEW_TAG;
31 
32 import com.android.SdkConstants;
33 import com.android.annotations.NonNull;
34 import com.android.ide.common.xml.ManifestData;
35 import com.android.ide.eclipse.adt.AdtConstants;
36 import com.android.ide.eclipse.adt.AdtPlugin;
37 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
38 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
39 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
40 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
41 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
42 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
43 import com.android.resources.ResourceFolderType;
44 import com.android.resources.ResourceType;
45 import com.android.utils.SdkUtils;
46 
47 import org.eclipse.core.resources.IFile;
48 import org.eclipse.core.resources.IFolder;
49 import org.eclipse.core.resources.IProject;
50 import org.eclipse.core.resources.IResource;
51 import org.eclipse.core.runtime.CoreException;
52 import org.eclipse.core.runtime.IProgressMonitor;
53 import org.eclipse.core.runtime.NullProgressMonitor;
54 import org.eclipse.core.runtime.OperationCanceledException;
55 import org.eclipse.jdt.core.IField;
56 import org.eclipse.jdt.core.IJavaElement;
57 import org.eclipse.jdt.core.IJavaProject;
58 import org.eclipse.jdt.core.IType;
59 import org.eclipse.jdt.core.ITypeHierarchy;
60 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameCompilationUnitProcessor;
61 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor;
62 import org.eclipse.ltk.core.refactoring.Change;
63 import org.eclipse.ltk.core.refactoring.CompositeChange;
64 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
65 import org.eclipse.ltk.core.refactoring.TextFileChange;
66 import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
67 import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor;
68 import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
69 import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
70 import org.eclipse.text.edits.MultiTextEdit;
71 import org.eclipse.text.edits.ReplaceEdit;
72 import org.eclipse.text.edits.TextEdit;
73 import org.eclipse.wst.sse.core.StructuredModelManager;
74 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
75 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
76 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
77 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
78 import org.w3c.dom.Attr;
79 import org.w3c.dom.Element;
80 import org.w3c.dom.NamedNodeMap;
81 import org.w3c.dom.Node;
82 import org.w3c.dom.NodeList;
83 
84 import java.io.IOException;
85 import java.util.ArrayList;
86 import java.util.Collection;
87 import java.util.List;
88 
89 /**
90  * A participant to participate in refactorings that rename a type in an Android project.
91  * The class updates android manifest and the layout file
92  * The user can suppress refactoring by disabling the "Update references" checkbox.
93  * <p>
94  * Rename participants are registered via the extension point <code>
95  * org.eclipse.ltk.core.refactoring.renameParticipants</code>.
96  * Extensions to this extension point must therefore extend
97  * <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>.
98  */
99 @SuppressWarnings("restriction")
100 public class AndroidTypeRenameParticipant extends RenameParticipant {
101     private IProject mProject;
102     private IFile mManifestFile;
103     private String mOldFqcn;
104     private String mNewFqcn;
105     private String mOldSimpleName;
106     private String mNewSimpleName;
107     private String mOldDottedName;
108     private String mNewDottedName;
109     private boolean mIsCustomView;
110 
111     /**
112      * Set while we are creating an embedded Java refactoring. This could cause a recursive
113      * invocation of the XML renaming refactoring to react to the field, so this is flag
114      * during the call to the Java processor, and is used to ignore requests for adding in
115      * field reactions during that time.
116      */
117     private static boolean sIgnore;
118 
119     @Override
getName()120     public String getName() {
121         return "Android Type Rename";
122     }
123 
124     @Override
checkConditions(IProgressMonitor pm, CheckConditionsContext context)125     public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
126             throws OperationCanceledException {
127         return new RefactoringStatus();
128     }
129 
130     @Override
initialize(Object element)131     protected boolean initialize(Object element) {
132         if (sIgnore) {
133             return false;
134         }
135 
136         if (element instanceof IType) {
137             IType type = (IType) element;
138             IJavaProject javaProject = (IJavaProject) type.getAncestor(IJavaElement.JAVA_PROJECT);
139             mProject = javaProject.getProject();
140             IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP
141                     + SdkConstants.FN_ANDROID_MANIFEST_XML);
142 
143             if (manifestResource == null || !manifestResource.exists()
144                     || !(manifestResource instanceof IFile)) {
145                 RefactoringUtil.logInfo(
146                         String.format("Invalid or missing file %1$s in project %2$s",
147                                 SdkConstants.FN_ANDROID_MANIFEST_XML,
148                                 mProject.getName()));
149                 return false;
150             }
151 
152             try {
153                 IType classView = javaProject.findType(CLASS_VIEW);
154                 if (classView != null) {
155                     ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
156                     if (hierarchy.contains(classView)) {
157                         mIsCustomView = true;
158                     }
159                 }
160             } catch (CoreException e) {
161                 AdtPlugin.log(e, null);
162             }
163 
164             mManifestFile = (IFile) manifestResource;
165             ManifestData manifestData;
166             manifestData = AndroidManifestHelper.parseForData(mManifestFile);
167             if (manifestData == null) {
168                 return false;
169             }
170             mOldSimpleName = type.getElementName();
171             mOldDottedName = '.' + mOldSimpleName;
172             mOldFqcn = type.getFullyQualifiedName();
173             String packageName = type.getPackageFragment().getElementName();
174             mNewSimpleName = getArguments().getNewName();
175             mNewDottedName = '.' + mNewSimpleName;
176             if (packageName != null) {
177                 mNewFqcn = packageName + mNewDottedName;
178             } else {
179                 mNewFqcn = mNewSimpleName;
180             }
181             if (mOldFqcn == null || mNewFqcn == null) {
182                 return false;
183             }
184             if (!RefactoringUtil.isRefactorAppPackage() && mNewFqcn.indexOf('.') == -1) {
185                 mNewFqcn = packageName + mNewDottedName;
186             }
187             return true;
188         }
189         return false;
190     }
191 
192     @Override
createChange(IProgressMonitor pm)193     public Change createChange(IProgressMonitor pm) throws CoreException,
194             OperationCanceledException {
195         if (pm.isCanceled()) {
196             return null;
197         }
198 
199         // Only propose this refactoring if the "Update References" checkbox is set.
200         if (!getArguments().getUpdateReferences()) {
201             return null;
202         }
203 
204         RefactoringProcessor p = getProcessor();
205         if (p instanceof RenameCompilationUnitProcessor) {
206             RenameTypeProcessor rtp =
207                     ((RenameCompilationUnitProcessor) p).getRenameTypeProcessor();
208             if (rtp != null) {
209                 String pattern = rtp.getFilePatterns();
210                 boolean updQualf = rtp.getUpdateQualifiedNames();
211                 if (updQualf && pattern != null && pattern.contains("xml")) { //$NON-NLS-1$
212                     // Do not propose this refactoring if the
213                     // "Update fully qualified names in non-Java files" option is
214                     // checked and the file patterns mention XML. [c.f. SDK bug 21589]
215                     return null;
216                 }
217             }
218         }
219 
220         CompositeChange result = new CompositeChange(getName());
221 
222         // Only show the children in the refactoring preview dialog
223         result.markAsSynthetic();
224 
225         addManifestFileChanges(mManifestFile, result);
226         addLayoutFileChanges(mProject, result);
227         addJavaChanges(mProject, result, pm);
228 
229         // Also update in dependent projects
230         // TODO: Also do the Java elements, if they are in Jar files, since the library
231         // projects do this (and the JDT refactoring does not include them)
232         ProjectState projectState = Sdk.getProjectState(mProject);
233         if (projectState != null) {
234             Collection<ProjectState> parentProjects = projectState.getFullParentProjects();
235             for (ProjectState parentProject : parentProjects) {
236                 IProject project = parentProject.getProject();
237                 IResource manifestResource = project.findMember(AdtConstants.WS_SEP
238                         + SdkConstants.FN_ANDROID_MANIFEST_XML);
239                 if (manifestResource != null && manifestResource.exists()
240                         && manifestResource instanceof IFile) {
241                     addManifestFileChanges((IFile) manifestResource, result);
242                 }
243                 addLayoutFileChanges(project, result);
244                 addJavaChanges(project, result, pm);
245             }
246         }
247 
248         // Look for the field change on the R.java class; it's a derived file
249         // and will generate file modified manually warnings. Disable it.
250         RenameResourceParticipant.disableRClassChanges(result);
251 
252         return (result.getChildren().length == 0) ? null : result;
253     }
254 
addJavaChanges(IProject project, CompositeChange result, IProgressMonitor monitor)255     private void addJavaChanges(IProject project, CompositeChange result, IProgressMonitor monitor) {
256         if (!mIsCustomView) {
257             return;
258         }
259 
260         // Also rename styleables, if any
261         try {
262             // Find R class
263             IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
264             ManifestInfo info = ManifestInfo.get(project);
265             info.getPackage();
266             String rFqcn = info.getPackage() + '.' + R_CLASS;
267             IType styleable = javaProject.findType(rFqcn + '.' + ResourceType.STYLEABLE.getName());
268             if (styleable != null) {
269                 IField[] fields = styleable.getFields();
270                 CompositeChange fieldChanges = null;
271                 for (IField field : fields) {
272                     String name = field.getElementName();
273                     if (name.equals(mOldSimpleName) || name.startsWith(mOldSimpleName)
274                             && name.length() > mOldSimpleName.length()
275                             && name.charAt(mOldSimpleName.length()) == '_') {
276                         // Rename styleable fields
277                         String newName = name.equals(mOldSimpleName) ? mNewSimpleName :
278                             mNewSimpleName + name.substring(mOldSimpleName.length());
279                         RenameRefactoring refactoring =
280                                 RenameResourceParticipant.createFieldRefactoring(field,
281                                         newName, true);
282 
283                         try {
284                             sIgnore = true;
285                             RefactoringStatus status = refactoring.checkAllConditions(monitor);
286                             if (status != null && !status.hasError()) {
287                                 Change fieldChange = refactoring.createChange(monitor);
288                                 if (fieldChange != null) {
289                                     if (fieldChanges == null) {
290                                         fieldChanges = new CompositeChange(
291                                                 "Update custom view styleable fields");
292                                         // Disable these changes. They sometimes end up
293                                         // editing the wrong offsets. It looks like Eclipse
294                                         // doesn't ensure that after applying each change it
295                                         // also adjusts the other field offsets. I poked around
296                                         // and couldn't find a way to do this properly, but
297                                         // at least by listing the diffs here it shows what should
298                                         // be done.
299                                         fieldChanges.setEnabled(false);
300                                     }
301                                     // Disable change: see comment above.
302                                     fieldChange.setEnabled(false);
303                                     fieldChanges.add(fieldChange);
304                                 }
305                             }
306                         } catch (CoreException e) {
307                             AdtPlugin.log(e, null);
308                         } finally {
309                             sIgnore = false;
310                         }
311                     }
312                 }
313                 if (fieldChanges != null) {
314                     result.add(fieldChanges);
315                 }
316             }
317         } catch (CoreException e) {
318             AdtPlugin.log(e, null);
319         }
320     }
321 
addManifestFileChanges(IFile manifestFile, CompositeChange result)322     private void addManifestFileChanges(IFile manifestFile, CompositeChange result) {
323         addXmlFileChanges(manifestFile, result, null);
324     }
325 
addLayoutFileChanges(IProject project, CompositeChange result)326     private void addLayoutFileChanges(IProject project, CompositeChange result) {
327         try {
328             // Update references in XML resource files
329             IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
330 
331             IResource[] folders = resFolder.members();
332             for (IResource folder : folders) {
333                 String folderName = folder.getName();
334                 ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
335                 if (folderType != ResourceFolderType.LAYOUT &&
336                         folderType != ResourceFolderType.VALUES) {
337                     continue;
338                 }
339                 if (!(folder instanceof IFolder)) {
340                     continue;
341                 }
342                 IResource[] files = ((IFolder) folder).members();
343                 for (int i = 0; i < files.length; i++) {
344                     IResource member = files[i];
345                     if ((member instanceof IFile) && member.exists()) {
346                         IFile file = (IFile) member;
347                         String fileName = member.getName();
348 
349                         if (SdkUtils.endsWith(fileName, DOT_XML)) {
350                             addXmlFileChanges(file, result, folderType);
351                         }
352                     }
353                 }
354             }
355         } catch (CoreException e) {
356             RefactoringUtil.log(e);
357         }
358     }
359 
addXmlFileChanges(IFile file, CompositeChange changes, ResourceFolderType folderType)360     private boolean addXmlFileChanges(IFile file, CompositeChange changes,
361             ResourceFolderType folderType) {
362         IModelManager modelManager = StructuredModelManager.getModelManager();
363         IStructuredModel model = null;
364         try {
365             model = modelManager.getExistingModelForRead(file);
366             if (model == null) {
367                 model = modelManager.getModelForRead(file);
368             }
369             if (model != null) {
370                 IStructuredDocument document = model.getStructuredDocument();
371                 if (model instanceof IDOMModel) {
372                     IDOMModel domModel = (IDOMModel) model;
373                     Element root = domModel.getDocument().getDocumentElement();
374                     if (root != null) {
375                         List<TextEdit> edits = new ArrayList<TextEdit>();
376                         if (folderType == null) {
377                             assert file.getName().equals(ANDROID_MANIFEST_XML);
378                             addManifestReplacements(edits, root, document);
379                         } else if (folderType == ResourceFolderType.VALUES) {
380                             addValueReplacements(edits, root, document);
381                         } else {
382                             assert folderType == ResourceFolderType.LAYOUT;
383                             addLayoutReplacements(edits, root, document);
384                         }
385                         if (!edits.isEmpty()) {
386                             MultiTextEdit rootEdit = new MultiTextEdit();
387                             rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
388                             TextFileChange change = new TextFileChange(file.getName(), file);
389                             change.setTextType(EXT_XML);
390                             change.setEdit(rootEdit);
391                             changes.add(change);
392                         }
393                     }
394                 } else {
395                     return false;
396                 }
397             }
398 
399             return true;
400         } catch (IOException e) {
401             AdtPlugin.log(e, null);
402         } catch (CoreException e) {
403             AdtPlugin.log(e, null);
404         } finally {
405             if (model != null) {
406                 model.releaseFromRead();
407             }
408         }
409 
410         return false;
411     }
412 
addLayoutReplacements( @onNull List<TextEdit> edits, @NonNull Element element, @NonNull IStructuredDocument document)413     private void addLayoutReplacements(
414             @NonNull List<TextEdit> edits,
415             @NonNull Element element,
416             @NonNull IStructuredDocument document) {
417         String tag = element.getTagName();
418         if (tag.equals(mOldFqcn)) {
419             int start = RefactoringUtil.getTagNameRangeStart(element, document);
420             if (start != -1) {
421                 int end = start + mOldFqcn.length();
422                 edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
423             }
424         } else if (tag.equals(VIEW_TAG)) {
425             // TODO: Handle inner classes ($ vs .) ?
426             Attr classNode = element.getAttributeNode(ATTR_CLASS);
427             if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
428                 int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
429                 if (start != -1) {
430                     int end = start + mOldFqcn.length();
431                     edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
432                 }
433             }
434         } else if (tag.equals(VIEW_FRAGMENT)) {
435             Attr classNode = element.getAttributeNode(ATTR_CLASS);
436             if (classNode == null) {
437                 classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
438             }
439             if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
440                 int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
441                 if (start != -1) {
442                     int end = start + mOldFqcn.length();
443                     edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
444                 }
445             }
446         } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) {
447             Attr classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT);
448             if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
449                 int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
450                 if (start != -1) {
451                     int end = start + mOldFqcn.length();
452                     edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
453                 }
454             } else if (classNode != null && classNode.getValue().equals(mOldDottedName)) {
455                 int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
456                 if (start != -1) {
457                     int end = start + mOldDottedName.length();
458                     edits.add(new ReplaceEdit(start, end - start, mNewDottedName));
459                 }
460             }
461         }
462 
463         NodeList children = element.getChildNodes();
464         for (int i = 0, n = children.getLength(); i < n; i++) {
465             Node child = children.item(i);
466             if (child.getNodeType() == Node.ELEMENT_NODE) {
467                 addLayoutReplacements(edits, (Element) child, document);
468             }
469         }
470     }
471 
addValueReplacements( @onNull List<TextEdit> edits, @NonNull Element root, @NonNull IStructuredDocument document)472     private void addValueReplacements(
473             @NonNull List<TextEdit> edits,
474             @NonNull Element root,
475             @NonNull IStructuredDocument document) {
476         // Look for styleable renames for custom views
477         String declareStyleable = ResourceType.DECLARE_STYLEABLE.getName();
478         List<Element> topLevel = DomUtilities.getChildren(root);
479         for (Element element : topLevel) {
480             String tag = element.getTagName();
481             if (declareStyleable.equals(tag)) {
482                 Attr nameNode = element.getAttributeNode(ATTR_NAME);
483                 if (nameNode != null && mOldSimpleName.equals(nameNode.getValue())) {
484                     int start = RefactoringUtil.getAttributeValueRangeStart(nameNode, document);
485                     if (start != -1) {
486                         int end = start + mOldSimpleName.length();
487                         edits.add(new ReplaceEdit(start, end - start, mNewSimpleName));
488                     }
489                 }
490             }
491         }
492     }
493 
addManifestReplacements( @onNull List<TextEdit> edits, @NonNull Element element, @NonNull IStructuredDocument document)494     private void addManifestReplacements(
495             @NonNull List<TextEdit> edits,
496             @NonNull Element element,
497             @NonNull IStructuredDocument document) {
498         NamedNodeMap attributes = element.getAttributes();
499         for (int i = 0, n = attributes.getLength(); i < n; i++) {
500             Attr attr = (Attr) attributes.item(i);
501             if (!RefactoringUtil.isManifestClassAttribute(attr)) {
502                 continue;
503             }
504 
505             String value = attr.getValue();
506             if (value.equals(mOldFqcn)) {
507                 int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
508                 if (start != -1) {
509                     int end = start + mOldFqcn.length();
510                     edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
511                 }
512             } else if (value.equals(mOldDottedName)) {
513                 int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
514                 if (start != -1) {
515                     int end = start + mOldDottedName.length();
516                     edits.add(new ReplaceEdit(start, end - start, mNewDottedName));
517                 }
518             }
519         }
520 
521         NodeList children = element.getChildNodes();
522         for (int i = 0, n = children.getLength(); i < n; i++) {
523             Node child = children.item(i);
524             if (child.getNodeType() == Node.ELEMENT_NODE) {
525                 addManifestReplacements(edits, (Element) child, document);
526             }
527         }
528     }
529 }