• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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.extractstring;
18 
19 import static com.android.SdkConstants.QUOT_ENTITY;
20 import static com.android.SdkConstants.STRING_PREFIX;
21 
22 import com.android.SdkConstants;
23 import com.android.ide.common.res2.ValueXmlHelper;
24 import com.android.ide.common.xml.ManifestData;
25 import com.android.ide.eclipse.adt.AdtConstants;
26 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
27 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
28 import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor;
29 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
30 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
31 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
32 import com.android.resources.ResourceFolderType;
33 import com.android.resources.ResourceType;
34 
35 import org.eclipse.core.resources.IContainer;
36 import org.eclipse.core.resources.IFile;
37 import org.eclipse.core.resources.IFolder;
38 import org.eclipse.core.resources.IProject;
39 import org.eclipse.core.resources.IResource;
40 import org.eclipse.core.resources.ResourceAttributes;
41 import org.eclipse.core.resources.ResourcesPlugin;
42 import org.eclipse.core.runtime.CoreException;
43 import org.eclipse.core.runtime.IPath;
44 import org.eclipse.core.runtime.IProgressMonitor;
45 import org.eclipse.core.runtime.OperationCanceledException;
46 import org.eclipse.core.runtime.Path;
47 import org.eclipse.core.runtime.SubMonitor;
48 import org.eclipse.jdt.core.IBuffer;
49 import org.eclipse.jdt.core.ICompilationUnit;
50 import org.eclipse.jdt.core.IJavaProject;
51 import org.eclipse.jdt.core.IPackageFragment;
52 import org.eclipse.jdt.core.IPackageFragmentRoot;
53 import org.eclipse.jdt.core.JavaCore;
54 import org.eclipse.jdt.core.JavaModelException;
55 import org.eclipse.jdt.core.ToolFactory;
56 import org.eclipse.jdt.core.compiler.IScanner;
57 import org.eclipse.jdt.core.compiler.ITerminalSymbols;
58 import org.eclipse.jdt.core.compiler.InvalidInputException;
59 import org.eclipse.jdt.core.dom.AST;
60 import org.eclipse.jdt.core.dom.ASTNode;
61 import org.eclipse.jdt.core.dom.ASTParser;
62 import org.eclipse.jdt.core.dom.CompilationUnit;
63 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
64 import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
65 import org.eclipse.jface.text.ITextSelection;
66 import org.eclipse.ltk.core.refactoring.Change;
67 import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
68 import org.eclipse.ltk.core.refactoring.CompositeChange;
69 import org.eclipse.ltk.core.refactoring.Refactoring;
70 import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
71 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
72 import org.eclipse.ltk.core.refactoring.TextEditChangeGroup;
73 import org.eclipse.ltk.core.refactoring.TextFileChange;
74 import org.eclipse.text.edits.InsertEdit;
75 import org.eclipse.text.edits.MultiTextEdit;
76 import org.eclipse.text.edits.ReplaceEdit;
77 import org.eclipse.text.edits.TextEdit;
78 import org.eclipse.text.edits.TextEditGroup;
79 import org.eclipse.ui.IEditorPart;
80 import org.eclipse.wst.sse.core.StructuredModelManager;
81 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
82 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
83 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
84 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
85 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
86 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
87 import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
88 import org.w3c.dom.Node;
89 
90 import java.io.IOException;
91 import java.util.ArrayList;
92 import java.util.Arrays;
93 import java.util.HashMap;
94 import java.util.HashSet;
95 import java.util.Iterator;
96 import java.util.LinkedList;
97 import java.util.List;
98 import java.util.Map;
99 import java.util.Queue;
100 
101 /**
102  * This refactoring extracts a string from a file and replaces it by an Android resource ID
103  * such as R.string.foo.
104  * <p/>
105  * There are a number of scenarios, which are not all supported yet. The workflow works as
106  * such:
107  * <ul>
108  * <li> User selects a string in a Java and invokes the {@link ExtractStringAction}.
109  * <li> The action finds the {@link ICompilationUnit} being edited as well as the current
110  *      {@link ITextSelection}. The action creates a new instance of this refactoring as
111  *      well as an {@link ExtractStringWizard} and runs the operation.
112  * <li> Step 1 of the refactoring is to check the preliminary conditions. Right now we check
113  *      that the java source is not read-only and is in sync. We also try to find a string under
114  *      the selection. If this fails, the refactoring is aborted.
115  * <li> On success, the wizard is shown, which lets the user input the new ID to use.
116  * <li> The wizard sets the user input values into this refactoring instance, e.g. the new string
117  *      ID, the XML file to update, etc. The wizard does use the utility method
118  *      {@link XmlStringFileHelper#valueOfStringId(IProject, String, String)} to check whether
119  *      the new ID is already defined in the target XML file.
120  * <li> Once Preview or Finish is selected in the wizard, the
121  *      {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input
122  *      and compute the actual changes.
123  * <li> When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked.
124  * </ul>
125  *
126  * The list of changes are:
127  * <ul>
128  * <li> If the target XML does not exist, create it with the new string ID.
129  * <li> If the target XML exists, find the <resources> node and add the new string ID right after.
130  *      If the node is <resources/>, it needs to be opened.
131  * <li> Create an AST rewriter to edit the source Java file and replace all occurrences by the
132  *      new computed R.string.foo. Also need to rewrite imports to import R as needed.
133  *      If there's already a conflicting R included, we need to insert the FQCN instead.
134  * <li> TODO: Have a pref in the wizard: [x] Change other XML Files
135  * <li> TODO: Have a pref in the wizard: [x] Change other Java Files
136  * </ul>
137  */
138 @SuppressWarnings("restriction")
139 public class ExtractStringRefactoring extends Refactoring {
140 
141     public enum Mode {
142         /**
143          * the Extract String refactoring is called on an <em>existing</em> source file.
144          * Its purpose is then to get the selected string of the source and propose to
145          * change it by an XML id. The XML id may be a new one or an existing one.
146          */
147         EDIT_SOURCE,
148         /**
149          * The Extract String refactoring is called without any source file.
150          * Its purpose is then to create a new XML string ID or select/modify an existing one.
151          */
152         SELECT_ID,
153         /**
154          * The Extract String refactoring is called without any source file.
155          * Its purpose is then to create a new XML string ID. The ID must not already exist.
156          */
157         SELECT_NEW_ID
158     }
159 
160     /** The {@link Mode} of operation of the refactoring. */
161     private final Mode mMode;
162     /** Non-null when editing an Android Resource XML file: identifies the attribute name
163      * of the value being edited. When null, the source is an Android Java file. */
164     private String mXmlAttributeName;
165     /** The file model being manipulated.
166      * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */
167     private final IFile mFile;
168     /** The editor. Non-null when invoked from {@link ExtractStringAction}. Null otherwise. */
169     private final IEditorPart mEditor;
170     /** The project that contains {@link #mFile} and that contains the target XML file to modify. */
171     private final IProject mProject;
172     /** The start of the selection in {@link #mFile}.
173      * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */
174     private final int mSelectionStart;
175     /** The end of the selection in {@link #mFile}.
176      * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */
177     private final int mSelectionEnd;
178 
179     /** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */
180     private ICompilationUnit mUnit;
181     /** The actual string selected, after UTF characters have been escaped, good for display.
182      * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */
183     private String mTokenString;
184 
185     /** The XML string ID selected by the user in the wizard. */
186     private String mXmlStringId;
187     /** The XML string value. Might be different than the initial selected string. */
188     private String mXmlStringValue;
189     /** The path of the XML file that will define {@link #mXmlStringId}, selected by the user
190      *  in the wizard. This is relative to the project, e.g. "/res/values/string.xml" */
191     private String mTargetXmlFileWsPath;
192     /** True if we should find & replace in all Java files. */
193     private boolean mReplaceAllJava;
194     /** True if we should find & replace in all XML files of the same name in other res configs
195      * (other than the main {@link #mTargetXmlFileWsPath}.) */
196     private boolean mReplaceAllXml;
197 
198     /** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and
199      *  used by {@link #createChange(IProgressMonitor)}. */
200     private ArrayList<Change> mChanges;
201 
202     private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper();
203 
204     private static final String KEY_MODE = "mode";                      //$NON-NLS-1$
205     private static final String KEY_FILE = "file";                      //$NON-NLS-1$
206     private static final String KEY_PROJECT = "proj";                   //$NON-NLS-1$
207     private static final String KEY_SEL_START = "sel-start";            //$NON-NLS-1$
208     private static final String KEY_SEL_END = "sel-end";                //$NON-NLS-1$
209     private static final String KEY_TOK_ESC = "tok-esc";                //$NON-NLS-1$
210     private static final String KEY_XML_ATTR_NAME = "xml-attr-name";    //$NON-NLS-1$
211     private static final String KEY_RPLC_ALL_JAVA = "rplc-all-java";    //$NON-NLS-1$
212     private static final String KEY_RPLC_ALL_XML  = "rplc-all-xml";     //$NON-NLS-1$
213 
214     /**
215      * This constructor is solely used by {@link ExtractStringDescriptor},
216      * to replay a previous refactoring.
217      * <p/>
218      * To create a refactoring from code, please use one of the two other constructors.
219      *
220      * @param arguments A map previously created using {@link #createArgumentMap()}.
221      * @throws NullPointerException
222      */
ExtractStringRefactoring(Map<String, String> arguments)223     public ExtractStringRefactoring(Map<String, String> arguments) throws NullPointerException {
224 
225         mReplaceAllJava = Boolean.parseBoolean(arguments.get(KEY_RPLC_ALL_JAVA));
226         mReplaceAllXml  = Boolean.parseBoolean(arguments.get(KEY_RPLC_ALL_XML));
227         mMode = Mode.valueOf(arguments.get(KEY_MODE));
228 
229         IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT));
230         mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
231 
232         if (mMode == Mode.EDIT_SOURCE) {
233             path = Path.fromPortableString(arguments.get(KEY_FILE));
234             mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
235 
236             mSelectionStart   = Integer.parseInt(arguments.get(KEY_SEL_START));
237             mSelectionEnd     = Integer.parseInt(arguments.get(KEY_SEL_END));
238             mTokenString      = arguments.get(KEY_TOK_ESC);
239             mXmlAttributeName = arguments.get(KEY_XML_ATTR_NAME);
240         } else {
241             mFile = null;
242             mSelectionStart = mSelectionEnd = -1;
243             mTokenString = null;
244             mXmlAttributeName = null;
245         }
246 
247         mEditor = null;
248     }
249 
createArgumentMap()250     private Map<String, String> createArgumentMap() {
251         HashMap<String, String> args = new HashMap<String, String>();
252         args.put(KEY_RPLC_ALL_JAVA, Boolean.toString(mReplaceAllJava));
253         args.put(KEY_RPLC_ALL_XML,  Boolean.toString(mReplaceAllXml));
254         args.put(KEY_MODE,      mMode.name());
255         args.put(KEY_PROJECT,   mProject.getFullPath().toPortableString());
256         if (mMode == Mode.EDIT_SOURCE) {
257             args.put(KEY_FILE,      mFile.getFullPath().toPortableString());
258             args.put(KEY_SEL_START, Integer.toString(mSelectionStart));
259             args.put(KEY_SEL_END,   Integer.toString(mSelectionEnd));
260             args.put(KEY_TOK_ESC,   mTokenString);
261             args.put(KEY_XML_ATTR_NAME, mXmlAttributeName);
262         }
263         return args;
264     }
265 
266     /**
267      * Constructor to use when the Extract String refactoring is called on an
268      * *existing* source file. Its purpose is then to get the selected string of
269      * the source and propose to change it by an XML id. The XML id may be a new one
270      * or an existing one.
271      *
272      * @param file The source file to process. Cannot be null. File must exist in workspace.
273      * @param editor The editor.
274      * @param selection The selection in the source file. Cannot be null or empty.
275      */
ExtractStringRefactoring(IFile file, IEditorPart editor, ITextSelection selection)276     public ExtractStringRefactoring(IFile file, IEditorPart editor, ITextSelection selection) {
277         mMode = Mode.EDIT_SOURCE;
278         mFile = file;
279         mEditor = editor;
280         mProject = file.getProject();
281         mSelectionStart = selection.getOffset();
282         mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1);
283     }
284 
285     /**
286      * Constructor to use when the Extract String refactoring is called without
287      * any source file. Its purpose is then to create a new XML string ID.
288      * <p/>
289      * For example this is currently invoked by the ResourceChooser when
290      * the user wants to create a new string rather than select an existing one.
291      *
292      * @param project The project where the target XML file to modify is located. Cannot be null.
293      * @param enforceNew If true the XML ID must be a new one.
294      *                   If false, an existing ID can be used.
295      */
ExtractStringRefactoring(IProject project, boolean enforceNew)296     public ExtractStringRefactoring(IProject project, boolean enforceNew) {
297         mMode = enforceNew ? Mode.SELECT_NEW_ID : Mode.SELECT_ID;
298         mFile = null;
299         mEditor = null;
300         mProject = project;
301         mSelectionStart = mSelectionEnd = -1;
302     }
303 
304     /**
305      * Sets the replacement string ID. Used by the wizard to set the user input.
306      */
setNewStringId(String newStringId)307     public void setNewStringId(String newStringId) {
308         mXmlStringId = newStringId;
309     }
310 
311     /**
312      * Sets the replacement string ID. Used by the wizard to set the user input.
313      */
setNewStringValue(String newStringValue)314     public void setNewStringValue(String newStringValue) {
315         mXmlStringValue = newStringValue;
316     }
317 
318     /**
319      * Sets the target file. This is a project path, e.g. "/res/values/strings.xml".
320      * Used by the wizard to set the user input.
321      */
setTargetFile(String targetXmlFileWsPath)322     public void setTargetFile(String targetXmlFileWsPath) {
323         mTargetXmlFileWsPath = targetXmlFileWsPath;
324     }
325 
setReplaceAllJava(boolean replaceAllJava)326     public void setReplaceAllJava(boolean replaceAllJava) {
327         mReplaceAllJava = replaceAllJava;
328     }
329 
setReplaceAllXml(boolean replaceAllXml)330     public void setReplaceAllXml(boolean replaceAllXml) {
331         mReplaceAllXml = replaceAllXml;
332     }
333 
334     /**
335      * @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
336      */
337     @Override
getName()338     public String getName() {
339         if (mMode == Mode.SELECT_ID) {
340             return "Create or Use Android String";
341         } else if (mMode == Mode.SELECT_NEW_ID) {
342             return "Create New Android String";
343         }
344 
345         return "Extract Android String";
346     }
347 
getMode()348     public Mode getMode() {
349         return mMode;
350     }
351 
352     /**
353      * Gets the actual string selected, after UTF characters have been escaped,
354      * good for display. Value can be null.
355      */
getTokenString()356     public String getTokenString() {
357         return mTokenString;
358     }
359 
360     /** Returns the XML string ID selected by the user in the wizard. */
getXmlStringId()361     public String getXmlStringId() {
362         return mXmlStringId;
363     }
364 
365     /**
366      * Step 1 of 3 of the refactoring:
367      * Checks that the current selection meets the initial condition before the ExtractString
368      * wizard is shown. The check is supposed to be lightweight and quick. Note that at that
369      * point the wizard has not been created yet.
370      * <p/>
371      * Here we scan the source buffer to find the token matching the selection.
372      * The check is successful is a Java string literal is selected, the source is in sync
373      * and is not read-only.
374      * <p/>
375      * This is also used to extract the string to be modified, so that we can display it in
376      * the refactoring wizard.
377      *
378      * @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor)
379      *
380      * @throws CoreException
381      */
382     @Override
checkInitialConditions(IProgressMonitor monitor)383     public RefactoringStatus checkInitialConditions(IProgressMonitor monitor)
384             throws CoreException, OperationCanceledException {
385 
386         mUnit = null;
387         mTokenString = null;
388 
389         RefactoringStatus status = new RefactoringStatus();
390 
391         try {
392             monitor.beginTask("Checking preconditions...", 6);
393 
394             if (mMode != Mode.EDIT_SOURCE) {
395                 monitor.worked(6);
396                 return status;
397             }
398 
399             if (!checkSourceFile(mFile, status, monitor)) {
400                 return status;
401             }
402 
403             // Try to get a compilation unit from this file. If it fails, mUnit is null.
404             try {
405                 mUnit = JavaCore.createCompilationUnitFrom(mFile);
406 
407                 // Make sure the unit is not read-only, e.g. it's not a class file or inside a Jar
408                 if (mUnit.isReadOnly()) {
409                     status.addFatalError("The file is read-only, please make it writeable first.");
410                     return status;
411                 }
412 
413                 // This is a Java file. Check if it contains the selection we want.
414                 if (!findSelectionInJavaUnit(mUnit, status, monitor)) {
415                     return status;
416                 }
417 
418             } catch (Exception e) {
419                 // That was not a Java file. Ignore.
420             }
421 
422             if (mUnit != null) {
423                 monitor.worked(1);
424                 return status;
425             }
426 
427             // Check this a Layout XML file and get the selection and its context.
428             if (mFile != null && SdkConstants.EXT_XML.equals(mFile.getFileExtension())) {
429 
430                 // Currently we only support Android resource XML files, so they must have a path
431                 // similar to
432                 //    project/res/<type>[-<configuration>]/*.xml
433                 //    project/AndroidManifest.xml
434                 // There is no support for sub folders, so the segment count must be 4 or 2.
435                 // We don't need to check the type folder name because a/ we only accept
436                 // an AndroidXmlEditor source and b/ aapt generates a compilation error for
437                 // unknown folders.
438 
439                 IPath path = mFile.getFullPath();
440                 if ((path.segmentCount() == 4 &&
441                      path.segment(1).equalsIgnoreCase(SdkConstants.FD_RESOURCES)) ||
442                     (path.segmentCount() == 2 &&
443                      path.segment(1).equalsIgnoreCase(SdkConstants.FN_ANDROID_MANIFEST_XML))) {
444                     if (!findSelectionInXmlFile(mFile, status, monitor)) {
445                         return status;
446                     }
447                 }
448             }
449 
450             if (!status.isOK()) {
451                 status.addFatalError(
452                         "Selection must be inside a Java source or an Android Layout XML file.");
453             }
454 
455         } finally {
456             monitor.done();
457         }
458 
459         return status;
460     }
461 
462     /**
463      * Try to find the selected Java element in the compilation unit.
464      *
465      * If selection matches a string literal, capture it, otherwise add a fatal error
466      * to the status.
467      *
468      * On success, advance the monitor by 3.
469      * Returns status.isOK().
470      */
findSelectionInJavaUnit(ICompilationUnit unit, RefactoringStatus status, IProgressMonitor monitor)471     private boolean findSelectionInJavaUnit(ICompilationUnit unit,
472             RefactoringStatus status, IProgressMonitor monitor) {
473         try {
474             IBuffer buffer = unit.getBuffer();
475 
476             IScanner scanner = ToolFactory.createScanner(
477                     false, //tokenizeComments
478                     false, //tokenizeWhiteSpace
479                     false, //assertMode
480                     false  //recordLineSeparator
481                     );
482             scanner.setSource(buffer.getCharacters());
483             monitor.worked(1);
484 
485             for(int token = scanner.getNextToken();
486                     token != ITerminalSymbols.TokenNameEOF;
487                     token = scanner.getNextToken()) {
488                 if (scanner.getCurrentTokenStartPosition() <= mSelectionStart &&
489                         scanner.getCurrentTokenEndPosition() >= mSelectionEnd) {
490                     // found the token, but only keep if the right type
491                     if (token == ITerminalSymbols.TokenNameStringLiteral) {
492                         mTokenString = new String(scanner.getCurrentTokenSource());
493                     }
494                     break;
495                 } else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) {
496                     // scanner is past the selection, abort.
497                     break;
498                 }
499             }
500         } catch (JavaModelException e1) {
501             // Error in unit.getBuffer. Ignore.
502         } catch (InvalidInputException e2) {
503             // Error in scanner.getNextToken. Ignore.
504         } finally {
505             monitor.worked(1);
506         }
507 
508         if (mTokenString != null) {
509             // As a literal string, the token should have surrounding quotes. Remove them.
510             // Note: unquoteAttrValue technically removes either " or ' paired quotes, whereas
511             // the Java token should only have " quotes. Since we know the type to be a string
512             // literal, there should be no confusion here.
513             mTokenString = unquoteAttrValue(mTokenString);
514 
515             // We need a non-empty string literal
516             if (mTokenString.length() == 0) {
517                 mTokenString = null;
518             }
519         }
520 
521         if (mTokenString == null) {
522             status.addFatalError("Please select a Java string literal.");
523         }
524 
525         monitor.worked(1);
526         return status.isOK();
527     }
528 
529     /**
530      * Try to find the selected XML element. This implementation replies on the refactoring
531      * originating from an Android Layout Editor. We rely on some internal properties of the
532      * Structured XML editor to retrieve file content to avoid parsing it again. We also rely
533      * on our specific Android XML model to get element & attribute descriptor properties.
534      *
535      * If selection matches a string literal, capture it, otherwise add a fatal error
536      * to the status.
537      *
538      * On success, advance the monitor by 1.
539      * Returns status.isOK().
540      */
findSelectionInXmlFile(IFile file, RefactoringStatus status, IProgressMonitor monitor)541     private boolean findSelectionInXmlFile(IFile file,
542             RefactoringStatus status,
543             IProgressMonitor monitor) {
544 
545         try {
546             if (!(mEditor instanceof AndroidXmlEditor)) {
547                 status.addFatalError("Only the Android XML Editor is currently supported.");
548                 return status.isOK();
549             }
550 
551             AndroidXmlEditor editor = (AndroidXmlEditor) mEditor;
552             IStructuredModel smodel = null;
553             Node node = null;
554             String currAttrName = null;
555 
556             try {
557                 // See the portability note in AndroidXmlEditor#getModelForRead() javadoc.
558                 smodel = editor.getModelForRead();
559                 if (smodel != null) {
560                     // The structured model gives the us the actual XML Node element where the
561                     // offset is. By using this Node, we can find the exact UiElementNode of our
562                     // model and thus we'll be able to get the properties of the attribute -- to
563                     // check if it accepts a string reference. This does not however tell us if
564                     // the selection is actually in an attribute value, nor which attribute is
565                     // being edited.
566                     for(int offset = mSelectionStart; offset >= 0 && node == null; --offset) {
567                         node = (Node) smodel.getIndexedRegion(offset);
568                     }
569 
570                     if (node == null) {
571                         status.addFatalError(
572                                 "The selection does not match any element in the XML document.");
573                         return status.isOK();
574                     }
575 
576                     if (node.getNodeType() != Node.ELEMENT_NODE) {
577                         status.addFatalError("The selection is not inside an actual XML element.");
578                         return status.isOK();
579                     }
580 
581                     IStructuredDocument sdoc = smodel.getStructuredDocument();
582                     if (sdoc != null) {
583                         // Portability note: all the structured document implementation is
584                         // under wst.sse.core.internal.provisional so we can expect it to change in
585                         // a distant future if they start cleaning their codebase, however unlikely
586                         // that is.
587 
588                         int selStart = mSelectionStart;
589                         IStructuredDocumentRegion region =
590                             sdoc.getRegionAtCharacterOffset(selStart);
591                         if (region != null &&
592                                 DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
593                             // Find if any sub-region representing an attribute contains the
594                             // selection. If it does, returns the name of the attribute in
595                             // currAttrName and returns the value in the field mTokenString.
596                             currAttrName = findSelectionInRegion(region, selStart);
597 
598                             if (mTokenString == null) {
599                                 status.addFatalError(
600                                     "The selection is not inside an actual XML attribute value.");
601                             }
602                         }
603                     }
604 
605                     if (mTokenString != null && node != null && currAttrName != null) {
606 
607                         // Validate that the attribute accepts a string reference.
608                         // This sets mTokenString to null by side-effect when it fails and
609                         // adds a fatal error to the status as needed.
610                         validateSelectedAttribute(editor, node, currAttrName, status);
611 
612                     } else {
613                         // We shouldn't get here: we're missing one of the token string, the node
614                         // or the attribute name. All of them have been checked earlier so don't
615                         // set any specific error.
616                         mTokenString = null;
617                     }
618                 }
619             } catch (Throwable t) {
620                 // Since we use some internal APIs, use a broad catch-all to report any
621                 // unexpected issue rather than crash the whole refactoring.
622                 status.addFatalError(
623                         String.format("XML parsing error: %1$s", t.getMessage()));
624             } finally {
625                 if (smodel != null) {
626                     smodel.releaseFromRead();
627                 }
628             }
629 
630         } finally {
631             monitor.worked(1);
632         }
633 
634         return status.isOK();
635     }
636 
637     /**
638      * The region gives us the textual representation of the XML element
639      * where the selection starts, split using sub-regions. We now just
640      * need to iterate through the sub-regions to find which one
641      * contains the actual selection. We're interested in an attribute
642      * value however when we find one we want to memorize the attribute
643      * name that was defined just before.
644      *
645      * @return When the cursor is on a valid attribute name or value, returns the string of
646      * attribute name. As a side-effect, returns the value of the attribute in {@link #mTokenString}
647      */
findSelectionInRegion(IStructuredDocumentRegion region, int selStart)648     private String findSelectionInRegion(IStructuredDocumentRegion region, int selStart) {
649 
650         String currAttrName = null;
651 
652         int startInRegion = selStart - region.getStartOffset();
653 
654         int nb = region.getNumberOfRegions();
655         ITextRegionList list = region.getRegions();
656         String currAttrValue = null;
657 
658         for (int i = 0; i < nb; i++) {
659             ITextRegion subRegion = list.get(i);
660             String type = subRegion.getType();
661 
662             if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
663                 currAttrName = region.getText(subRegion);
664 
665                 // I like to select the attribute definition and invoke
666                 // the extract string wizard. So if the selection is on
667                 // the attribute name part, find the value that is just
668                 // after and use it as if it were the selection.
669 
670                 if (subRegion.getStart() <= startInRegion &&
671                         startInRegion < subRegion.getTextEnd()) {
672                     // A well-formed attribute is composed of a name,
673                     // an equal sign and the value. There can't be any space
674                     // in between, which makes the parsing a lot easier.
675                     if (i <= nb - 3 &&
676                             DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals(
677                                                    list.get(i + 1).getType())) {
678                         subRegion = list.get(i + 2);
679                         type = subRegion.getType();
680                         if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(
681                                 type)) {
682                             currAttrValue = region.getText(subRegion);
683                         }
684                     }
685                 }
686 
687             } else if (subRegion.getStart() <= startInRegion &&
688                     startInRegion < subRegion.getTextEnd() &&
689                     DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
690                 currAttrValue = region.getText(subRegion);
691             }
692 
693             if (currAttrValue != null) {
694                 // We found the value. Only accept it if not empty
695                 // and if we found an attribute name before.
696                 String text = currAttrValue;
697 
698                 // The attribute value contains XML quotes. Remove them.
699                 text = unquoteAttrValue(text);
700                 if (text.length() > 0 && currAttrName != null) {
701                     // Setting mTokenString to non-null marks the fact we
702                     // accept this attribute.
703                     mTokenString = text;
704                 }
705 
706                 break;
707             }
708         }
709 
710         return currAttrName;
711     }
712 
713     /**
714      * Attribute values found as text for {@link DOMRegionContext#XML_TAG_ATTRIBUTE_VALUE}
715      * contain XML quotes. This removes the quotes (either single or double quotes).
716      *
717      * @param attrValue The attribute value, as extracted by
718      *                  {@link IStructuredDocumentRegion#getText(ITextRegion)}.
719      *                  Must not be null.
720      * @return The attribute value, without quotes. Whitespace is not trimmed, if any.
721      *         String may be empty, but not null.
722      */
unquoteAttrValue(String attrValue)723     static String unquoteAttrValue(String attrValue) {
724         int len = attrValue.length();
725         int len1 = len - 1;
726         if (len >= 2 &&
727                 attrValue.charAt(0) == '"' &&
728                 attrValue.charAt(len1) == '"') {
729             attrValue = attrValue.substring(1, len1);
730         } else if (len >= 2 &&
731                 attrValue.charAt(0) == '\'' &&
732                 attrValue.charAt(len1) == '\'') {
733             attrValue = attrValue.substring(1, len1);
734         }
735 
736         return attrValue;
737     }
738 
739     /**
740      * Validates that the attribute accepts a string reference.
741      * This sets mTokenString to null by side-effect when it fails and
742      * adds a fatal error to the status as needed.
743      */
validateSelectedAttribute(AndroidXmlEditor editor, Node node, String attrName, RefactoringStatus status)744     private void validateSelectedAttribute(AndroidXmlEditor editor, Node node,
745             String attrName, RefactoringStatus status) {
746         UiElementNode rootUiNode = editor.getUiRootNode();
747         UiElementNode currentUiNode =
748             rootUiNode == null ? null : rootUiNode.findXmlNode(node);
749         ReferenceAttributeDescriptor attrDesc = null;
750 
751         if (currentUiNode != null) {
752             // remove any namespace prefix from the attribute name
753             String name = attrName;
754             int pos = name.indexOf(':');
755             if (pos > 0 && pos < name.length() - 1) {
756                 name = name.substring(pos + 1);
757             }
758 
759             for (UiAttributeNode attrNode : currentUiNode.getAllUiAttributes()) {
760                 if (attrNode.getDescriptor().getXmlLocalName().equals(name)) {
761                     AttributeDescriptor desc = attrNode.getDescriptor();
762                     if (desc instanceof ReferenceAttributeDescriptor) {
763                         attrDesc = (ReferenceAttributeDescriptor) desc;
764                     }
765                     break;
766                 }
767             }
768         }
769 
770         // The attribute descriptor is a resource reference. It must either accept
771         // of any resource type or specifically accept string types.
772         if (attrDesc != null &&
773                 (attrDesc.getResourceType() == null ||
774                  attrDesc.getResourceType() == ResourceType.STRING)) {
775             // We have one more check to do: is the current string value already
776             // an Android XML string reference? If so, we can't edit it.
777             if (mTokenString != null && mTokenString.startsWith("@")) {                             //$NON-NLS-1$
778                 int pos1 = 0;
779                 if (mTokenString.length() > 1 && mTokenString.charAt(1) == '+') {
780                     pos1++;
781                 }
782                 int pos2 = mTokenString.indexOf('/');
783                 if (pos2 > pos1) {
784                     String kind = mTokenString.substring(pos1 + 1, pos2);
785                     if (ResourceType.STRING.getName().equals(kind)) {
786                         mTokenString = null;
787                         status.addFatalError(String.format(
788                                 "The attribute %1$s already contains a %2$s reference.",
789                                 attrName,
790                                 kind));
791                     }
792                 }
793             }
794 
795             if (mTokenString != null) {
796                 // We're done with all our checks. mTokenString contains the
797                 // current attribute value. We don't memorize the region nor the
798                 // attribute, however we memorize the textual attribute name so
799                 // that we can offer replacement for all its occurrences.
800                 mXmlAttributeName = attrName;
801             }
802 
803         } else {
804             mTokenString = null;
805             status.addFatalError(String.format(
806                     "The attribute %1$s does not accept a string reference.",
807                     attrName));
808         }
809     }
810 
811     /**
812      * Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit()
813      * Might not be useful.
814      *
815      * On success, advance the monitor by 2.
816      *
817      * @return False if caller should abort, true if caller should continue.
818      */
checkSourceFile(IFile file, RefactoringStatus status, IProgressMonitor monitor)819     private boolean checkSourceFile(IFile file,
820             RefactoringStatus status,
821             IProgressMonitor monitor) {
822         // check whether the source file is in sync
823         if (!file.isSynchronized(IResource.DEPTH_ZERO)) {
824             status.addFatalError("The file is not synchronized. Please save it first.");
825             return false;
826         }
827         monitor.worked(1);
828 
829         // make sure we can write to it.
830         ResourceAttributes resAttr = file.getResourceAttributes();
831         if (resAttr == null || resAttr.isReadOnly()) {
832             status.addFatalError("The file is read-only, please make it writeable first.");
833             return false;
834         }
835         monitor.worked(1);
836 
837         return true;
838     }
839 
840     /**
841      * Step 2 of 3 of the refactoring:
842      * Check the conditions once the user filled values in the refactoring wizard,
843      * then prepare the changes to be applied.
844      * <p/>
845      * In this case, most of the sanity checks are done by the wizard so essentially this
846      * should only be called if the wizard positively validated the user input.
847      *
848      * Here we do check that the target resource XML file either does not exists or
849      * is not read-only.
850      *
851      * @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor)
852      *
853      * @throws CoreException
854      */
855     @Override
checkFinalConditions(IProgressMonitor monitor)856     public RefactoringStatus checkFinalConditions(IProgressMonitor monitor)
857             throws CoreException, OperationCanceledException {
858         RefactoringStatus status = new RefactoringStatus();
859 
860         try {
861             monitor.beginTask("Checking post-conditions...", 5);
862 
863             if (mXmlStringId == null || mXmlStringId.length() <= 0) {
864                 // this is not supposed to happen
865                 status.addFatalError("Missing replacement string ID");
866             } else if (mTargetXmlFileWsPath == null || mTargetXmlFileWsPath.length() <= 0) {
867                 // this is not supposed to happen
868                 status.addFatalError("Missing target xml file path");
869             }
870             monitor.worked(1);
871 
872             // Either that resource must not exist or it must be a writable file.
873             IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath);
874             if (targetXml != null) {
875                 if (targetXml.getType() != IResource.FILE) {
876                     status.addFatalError(
877                             String.format("XML file '%1$s' is not a file.", mTargetXmlFileWsPath));
878                 } else {
879                     ResourceAttributes attr = targetXml.getResourceAttributes();
880                     if (attr != null && attr.isReadOnly()) {
881                         status.addFatalError(
882                                 String.format("XML file '%1$s' is read-only.",
883                                         mTargetXmlFileWsPath));
884                     }
885                 }
886             }
887             monitor.worked(1);
888 
889             if (status.hasError()) {
890                 return status;
891             }
892 
893             mChanges = new ArrayList<Change>();
894 
895 
896             // Prepare the change to create/edit the String ID in the res/values XML file.
897             if (!mXmlStringValue.equals(
898                     mXmlHelper.valueOfStringId(mProject, mTargetXmlFileWsPath, mXmlStringId))) {
899                 // We actually change it only if the ID doesn't exist yet or has a different value
900                 Change change = createXmlChanges((IFile) targetXml, mXmlStringId, mXmlStringValue,
901                         status, SubMonitor.convert(monitor, 1));
902                 if (change != null) {
903                     mChanges.add(change);
904                 }
905             }
906 
907             if (status.hasError()) {
908                 return status;
909             }
910 
911             if (mMode == Mode.EDIT_SOURCE) {
912                 List<Change> changes = null;
913                 if (mXmlAttributeName != null) {
914                     // Prepare the change to the Android resource XML file
915                     changes = computeXmlSourceChanges(mFile,
916                             mXmlStringId,
917                             mTokenString,
918                             mXmlAttributeName,
919                             true, // allConfigurations
920                             status,
921                             monitor);
922 
923                 } else if (mUnit != null) {
924                     // Prepare the change to the Java compilation unit
925                     changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
926                             status, SubMonitor.convert(monitor, 1));
927                 }
928                 if (changes != null) {
929                     mChanges.addAll(changes);
930                 }
931             }
932 
933             if (mReplaceAllJava) {
934                 String currentIdentifier = mUnit != null ? mUnit.getHandleIdentifier() : ""; //$NON-NLS-1$
935 
936                 SubMonitor submon = SubMonitor.convert(monitor, 1);
937                 for (ICompilationUnit unit : findAllJavaUnits()) {
938                     // Only process Java compilation units that exist, are not derived
939                     // and are not read-only.
940                     if (unit == null || !unit.exists()) {
941                         continue;
942                     }
943                     IResource resource = unit.getResource();
944                     if (resource == null || resource.isDerived()) {
945                         continue;
946                     }
947 
948                     // Ensure that we don't process the current compilation unit (processed
949                     // as mUnit above) twice
950                     if (currentIdentifier.equals(unit.getHandleIdentifier())) {
951                         continue;
952                     }
953 
954                     ResourceAttributes attrs = resource.getResourceAttributes();
955                     if (attrs != null && attrs.isReadOnly()) {
956                         continue;
957                     }
958 
959                     List<Change> changes = computeJavaChanges(
960                             unit, mXmlStringId, mTokenString,
961                             status, SubMonitor.convert(submon, 1));
962                     if (changes != null) {
963                         mChanges.addAll(changes);
964                     }
965                 }
966             }
967 
968             if (mReplaceAllXml) {
969                 SubMonitor submon = SubMonitor.convert(monitor, 1);
970                 for (IFile xmlFile : findAllResXmlFiles()) {
971                     if (xmlFile != null) {
972                         List<Change> changes = computeXmlSourceChanges(xmlFile,
973                                 mXmlStringId,
974                                 mTokenString,
975                                 mXmlAttributeName,
976                                 false, // allConfigurations
977                                 status,
978                                 SubMonitor.convert(submon, 1));
979                         if (changes != null) {
980                             mChanges.addAll(changes);
981                         }
982                     }
983                 }
984             }
985 
986             monitor.worked(1);
987         } finally {
988             monitor.done();
989         }
990 
991         return status;
992     }
993 
994     // --- XML changes ---
995 
996     /**
997      * Returns a foreach-compatible iterator over all XML files in the project's
998      * /res folder, excluding the target XML file (the one where we'll write/edit
999      * the string id).
1000      */
findAllResXmlFiles()1001     private Iterable<IFile> findAllResXmlFiles() {
1002         return new Iterable<IFile>() {
1003             @Override
1004             public Iterator<IFile> iterator() {
1005                 return new Iterator<IFile>() {
1006                     final Queue<IFile> mFiles = new LinkedList<IFile>();
1007                     final Queue<IResource> mFolders = new LinkedList<IResource>();
1008                     IPath mFilterPath1 = null;
1009                     IPath mFilterPath2 = null;
1010                     {
1011                         // Filter out the XML file where we'll be writing the XML string id.
1012                         IResource filterRes = mProject.findMember(mTargetXmlFileWsPath);
1013                         if (filterRes != null) {
1014                             mFilterPath1 = filterRes.getFullPath();
1015                         }
1016                         // Filter out the XML source file, if any (e.g. typically a layout)
1017                         if (mFile != null) {
1018                             mFilterPath2 = mFile.getFullPath();
1019                         }
1020 
1021                         // We want to process the manifest
1022                         IResource man = mProject.findMember("AndroidManifest.xml"); // TODO find a constant
1023                         if (man.exists() && man instanceof IFile && !man.equals(mFile)) {
1024                             mFiles.add((IFile) man);
1025                         }
1026 
1027                         // Add all /res folders (technically we don't need to process /res/values
1028                         // XML files that contain resources/string elements, but it's easier to
1029                         // not filter them out.)
1030                         IFolder f = mProject.getFolder(AdtConstants.WS_RESOURCES);
1031                         if (f.exists()) {
1032                             try {
1033                                 mFolders.addAll(
1034                                         Arrays.asList(f.members(IContainer.EXCLUDE_DERIVED)));
1035                             } catch (CoreException e) {
1036                                 // pass
1037                             }
1038                         }
1039                     }
1040 
1041                     @Override
1042                     public boolean hasNext() {
1043                         if (!mFiles.isEmpty()) {
1044                             return true;
1045                         }
1046 
1047                         while (!mFolders.isEmpty()) {
1048                             IResource res = mFolders.poll();
1049                             if (res.exists() && res instanceof IFolder) {
1050                                 IFolder f = (IFolder) res;
1051                                 try {
1052                                     getFileList(f);
1053                                     if (!mFiles.isEmpty()) {
1054                                         return true;
1055                                     }
1056                                 } catch (CoreException e) {
1057                                     // pass
1058                                 }
1059                             }
1060                         }
1061                         return false;
1062                     }
1063 
1064                     private void getFileList(IFolder folder) throws CoreException {
1065                         for (IResource res : folder.members(IContainer.EXCLUDE_DERIVED)) {
1066                             // Only accept file resources which are not derived and actually exist
1067                             if (res.exists() && !res.isDerived() && res instanceof IFile) {
1068                                 IFile file = (IFile) res;
1069                                 // Must have an XML extension
1070                                 if (SdkConstants.EXT_XML.equals(file.getFileExtension())) {
1071                                     IPath p = file.getFullPath();
1072                                     // And not be either paths we want to filter out
1073                                     if ((mFilterPath1 != null && mFilterPath1.equals(p)) ||
1074                                             (mFilterPath2 != null && mFilterPath2.equals(p))) {
1075                                         continue;
1076                                     }
1077                                     mFiles.add(file);
1078                                 }
1079                             }
1080                         }
1081                     }
1082 
1083                     @Override
1084                     public IFile next() {
1085                         IFile file = mFiles.poll();
1086                         hasNext();
1087                         return file;
1088                     }
1089 
1090                     @Override
1091                     public void remove() {
1092                         throw new UnsupportedOperationException(
1093                             "This iterator does not support removal");  //$NON-NLS-1$
1094                     }
1095                 };
1096             }
1097         };
1098     }
1099 
1100     /**
1101      * Internal helper that actually prepares the {@link Change} that adds the given
1102      * ID to the given XML File.
1103      * <p/>
1104      * This does not actually modify the file.
1105      *
1106      * @param targetXml The file resource to modify.
1107      * @param xmlStringId The new ID to insert.
1108      * @param tokenString The old string, which will be the value in the XML string.
1109      * @return A new {@link TextEdit} that describes how to change the file.
1110      */
1111     private Change createXmlChanges(IFile targetXml,
1112             String xmlStringId,
1113             String tokenString,
1114             RefactoringStatus status,
1115             SubMonitor monitor) {
1116 
1117         TextFileChange xmlChange = new TextFileChange(getName(), targetXml);
1118         xmlChange.setTextType(SdkConstants.EXT_XML);
1119 
1120         String error = "";                  //$NON-NLS-1$
1121         TextEdit edit = null;
1122         TextEditGroup editGroup = null;
1123 
1124         try {
1125             if (!targetXml.exists()) {
1126                 // Kludge: use targetXml==null as a signal this is a new file being created
1127                 targetXml = null;
1128             }
1129 
1130             edit = createXmlReplaceEdit(targetXml, xmlStringId, tokenString, status,
1131                     SubMonitor.convert(monitor, 1));
1132         } catch (IOException e) {
1133             error = e.toString();
1134         } catch (CoreException e) {
1135             // Failed to read file. Ignore. Will handle error below.
1136             error = e.toString();
1137         }
1138 
1139         if (edit == null) {
1140             status.addFatalError(String.format("Failed to modify file %1$s%2$s",
1141                     targetXml == null ? "" : targetXml.getFullPath(),   //$NON-NLS-1$
1142                     error == null ? "" : ": " + error));                //$NON-NLS-1$
1143             return null;
1144         }
1145 
1146         editGroup = new TextEditGroup(targetXml == null ? "Create <string> in new XML file"
1147                                                         : "Insert <string> in XML file",
1148                                       edit);
1149 
1150         xmlChange.setEdit(edit);
1151         // The TextEditChangeGroup let the user toggle this change on and off later.
1152         xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup));
1153 
1154         monitor.worked(1);
1155         return xmlChange;
1156     }
1157 
1158     /**
1159      * Scan the XML file to find the best place where to insert the new string element.
1160      * <p/>
1161      * This handles a variety of cases, including replacing existing ids in place,
1162      * adding the top resources element if missing and the XML PI if not present.
1163      * It tries to preserve indentation when adding new elements at the end of an existing XML.
1164      *
1165      * @param file The XML file to modify, that must be present in the workspace.
1166      *             Pass null to create a change for a new file that doesn't exist yet.
1167      * @param xmlStringId The new ID to insert.
1168      * @param tokenString The old string, which will be the value in the XML string.
1169      * @param status The in-out refactoring status. Used to log a more detailed error if the
1170      *          XML has a top element that is not a resources element.
1171      * @param monitor A monitor to track progress.
1172      * @return A new {@link TextEdit} for either a replace or an insert operation, or null in case
1173      *          of error.
1174      * @throws CoreException - if the file's contents or description can not be read.
1175      * @throws IOException   - if the file's contents can not be read or its detected encoding does
1176      *                         not support its contents.
1177      */
1178     private TextEdit createXmlReplaceEdit(IFile file,
1179             String xmlStringId,
1180             String tokenString,
1181             RefactoringStatus status,
1182             SubMonitor monitor)
1183                 throws IOException, CoreException {
1184 
1185         IModelManager modelMan = StructuredModelManager.getModelManager();
1186 
1187         final String NODE_RESOURCES = SdkConstants.TAG_RESOURCES;
1188         final String NODE_STRING = SdkConstants.TAG_STRING;
1189         final String ATTR_NAME = SdkConstants.ATTR_NAME;
1190 
1191 
1192         // Scan the source to find the best insertion point.
1193 
1194         // 1- The most common case we need to handle is the one of inserting at the end
1195         //    of a valid XML document, respecting the whitespace last used.
1196         //
1197         // Ideally we have this structure:
1198         // <xml ...>
1199         // <resource>
1200         // ...ws1...<string>blah</string>...ws2...
1201         // </resource>
1202         //
1203         // where ws1 and ws2 are the whitespace respectively before and after the last element
1204         // just before the closing </resource>.
1205         // In this case we want to generate the new string just before ws2...</resource> with
1206         // the same whitespace as ws1.
1207         //
1208         // 2- Another expected case is there's already an existing string which "name" attribute
1209         //    equals to xmlStringId and we just want to replace its value.
1210         //
1211         // Other cases we need to handle:
1212         // 3- There is no element at all -> create a full new <resource>+<string> content.
1213         // 4- There is <resource/>, that is the tag is not opened. This can be handled as the
1214         //    previous case, generating full content but also replacing <resource/>.
1215         // 5- There is a top element that is not <resource>. That's a fatal error and we abort.
1216 
1217         IStructuredModel smodel = null;
1218 
1219         // Single and double quotes must be escaped in the <string>value</string> declaration
1220         tokenString = ValueXmlHelper.escapeResourceString(tokenString);
1221 
1222         try {
1223             IStructuredDocument sdoc = null;
1224             boolean checkTopElement = true;
1225             boolean replaceStringContent = false;
1226             boolean hasPiXml = false;
1227             int newResStart = 0;
1228             int newResLength = 0;
1229             String lineSep = "\n";                  //$NON-NLS-1$
1230 
1231             if (file != null) {
1232                 smodel = modelMan.getExistingModelForRead(file);
1233                 if (smodel != null) {
1234                     sdoc = smodel.getStructuredDocument();
1235                 } else if (smodel == null) {
1236                     // The model is not currently open.
1237                     if (file.exists()) {
1238                         sdoc = modelMan.createStructuredDocumentFor(file);
1239                     } else {
1240                         sdoc = modelMan.createNewStructuredDocumentFor(file);
1241                     }
1242                 }
1243             }
1244 
1245             if (sdoc == null && file != null) {
1246                 // Get a document matching the actual saved file
1247                 sdoc = modelMan.createStructuredDocumentFor(file);
1248             }
1249 
1250             if (sdoc != null) {
1251                 String wsBefore = "";   //$NON-NLS-1$
1252                 String lastWs = null;
1253 
1254                 lineSep = sdoc.getLineDelimiter();
1255                 if (lineSep == null || lineSep.length() == 0) {
1256                     // That wasn't too useful, let's go back to a reasonable default
1257                     lineSep = "\n"; //$NON-NLS-1$
1258                 }
1259 
1260                 for (IStructuredDocumentRegion regions : sdoc.getStructuredDocumentRegions()) {
1261                     String type = regions.getType();
1262 
1263                     if (DOMRegionContext.XML_CONTENT.equals(type)) {
1264 
1265                         if (replaceStringContent) {
1266                             // Generate a replacement for a <string> value matching the string ID.
1267                             return new ReplaceEdit(
1268                                     regions.getStartOffset(), regions.getLength(), tokenString);
1269                         }
1270 
1271                         // Otherwise capture what should be whitespace content
1272                         lastWs = regions.getFullText();
1273                         continue;
1274 
1275                     } else if (DOMRegionContext.XML_PI_OPEN.equals(type) && !hasPiXml) {
1276 
1277                         int nb = regions.getNumberOfRegions();
1278                         ITextRegionList list = regions.getRegions();
1279                         for (int i = 0; i < nb; i++) {
1280                             ITextRegion region = list.get(i);
1281                             type = region.getType();
1282                             if (DOMRegionContext.XML_TAG_NAME.equals(type)) {
1283                                 String name = regions.getText(region);
1284                                 if ("xml".equals(name)) {   //$NON-NLS-1$
1285                                     hasPiXml = true;
1286                                     break;
1287                                 }
1288                             }
1289                         }
1290                         continue;
1291 
1292                     } else if (!DOMRegionContext.XML_TAG_NAME.equals(type)) {
1293                         // ignore things which are not a tag nor text content (such as comments)
1294                         continue;
1295                     }
1296 
1297                     int nb = regions.getNumberOfRegions();
1298                     ITextRegionList list = regions.getRegions();
1299 
1300                     String name = null;
1301                     String attrName = null;
1302                     String attrValue = null;
1303                     boolean isEmptyTag = false;
1304                     boolean isCloseTag = false;
1305 
1306                     for (int i = 0; i < nb; i++) {
1307                         ITextRegion region = list.get(i);
1308                         type = region.getType();
1309 
1310                         if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) {
1311                             isCloseTag = true;
1312                         } else if (DOMRegionContext.XML_EMPTY_TAG_CLOSE.equals(type)) {
1313                             isEmptyTag = true;
1314                         } else if (DOMRegionContext.XML_TAG_NAME.equals(type)) {
1315                             name = regions.getText(region);
1316                         } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type) &&
1317                                 NODE_STRING.equals(name)) {
1318                             // Record the attribute names into a <string> element.
1319                             attrName = regions.getText(region);
1320                         } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type) &&
1321                                 ATTR_NAME.equals(attrName)) {
1322                             // Record the value of a <string name=...> attribute
1323                             attrValue = regions.getText(region);
1324 
1325                             if (attrValue != null &&
1326                                     unquoteAttrValue(attrValue).equals(xmlStringId)) {
1327                                 // We found a <string name=> matching the string ID to replace.
1328                                 // We'll generate a replacement when we process the string value
1329                                 // (that is the next XML_CONTENT region.)
1330                                 replaceStringContent = true;
1331                             }
1332                         }
1333                     }
1334 
1335                     if (checkTopElement) {
1336                         // Check the top element has a resource name
1337                         checkTopElement = false;
1338                         if (!NODE_RESOURCES.equals(name)) {
1339                             status.addFatalError(
1340                                     String.format("XML file lacks a <resource> tag: %1$s",
1341                                             mTargetXmlFileWsPath));
1342                             return null;
1343 
1344                         }
1345 
1346                         if (isEmptyTag) {
1347                             // The top element is an empty "<resource/>" tag. We need to do
1348                             // a full new resource+string replacement.
1349                             newResStart = regions.getStartOffset();
1350                             newResLength = regions.getLength();
1351                         }
1352                     }
1353 
1354                     if (NODE_RESOURCES.equals(name)) {
1355                         if (isCloseTag) {
1356                             // We found the </resource> tag and we want
1357                             // to insert just before this one.
1358 
1359                             StringBuilder content = new StringBuilder();
1360                             content.append(wsBefore)
1361                                    .append("<string name=\"")                   //$NON-NLS-1$
1362                                    .append(xmlStringId)
1363                                    .append("\">")                               //$NON-NLS-1$
1364                                    .append(tokenString)
1365                                    .append("</string>");                        //$NON-NLS-1$
1366 
1367                             // Backup to insert before the whitespace preceding </resource>
1368                             IStructuredDocumentRegion insertBeforeReg = regions;
1369                             while (true) {
1370                                 IStructuredDocumentRegion previous = insertBeforeReg.getPrevious();
1371                                 if (previous != null &&
1372                                         DOMRegionContext.XML_CONTENT.equals(previous.getType()) &&
1373                                         previous.getText().trim().length() == 0) {
1374                                     insertBeforeReg = previous;
1375                                 } else {
1376                                     break;
1377                                 }
1378                             }
1379                             if (insertBeforeReg == regions) {
1380                                 // If we have not found any whitespace before </resources>,
1381                                 // at least add a line separator.
1382                                 content.append(lineSep);
1383                             }
1384 
1385                             return new InsertEdit(insertBeforeReg.getStartOffset(),
1386                                                   content.toString());
1387                         }
1388                     } else {
1389                         // For any other tag than <resource>, capture whitespace before and after.
1390                         if (!isCloseTag) {
1391                             wsBefore = lastWs;
1392                         }
1393                     }
1394                 }
1395             }
1396 
1397             // We reach here either because there's no XML content at all or because
1398             // there's an empty <resource/>.
1399             // Provide a full new resource+string replacement.
1400             StringBuilder content = new StringBuilder();
1401             if (!hasPiXml) {
1402                 content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); //$NON-NLS-1$
1403                 content.append(lineSep);
1404             } else if (newResLength == 0 && sdoc != null) {
1405                 // If inserting at the end, check if the last region is some whitespace.
1406                 // If there's no newline, insert one ourselves.
1407                 IStructuredDocumentRegion lastReg = sdoc.getLastStructuredDocumentRegion();
1408                 if (lastReg != null && lastReg.getText().indexOf('\n') == -1) {
1409                     content.append('\n');
1410                 }
1411             }
1412 
1413             // FIXME how to access formatting preferences to generate the proper indentation?
1414             content.append("<resources>").append(lineSep);                  //$NON-NLS-1$
1415             content.append("    <string name=\"")                           //$NON-NLS-1$
1416                    .append(xmlStringId)
1417                    .append("\">")                                           //$NON-NLS-1$
1418                    .append(tokenString)
1419                    .append("</string>")                                     //$NON-NLS-1$
1420                    .append(lineSep);
1421             content.append("</resources>").append(lineSep);                 //$NON-NLS-1$
1422 
1423             if (newResLength > 0) {
1424                 // Replace existing piece
1425                 return new ReplaceEdit(newResStart, newResLength, content.toString());
1426             } else {
1427                 // Insert at the end.
1428                 int offset = sdoc == null ? 0 : sdoc.getLength();
1429                 return new InsertEdit(offset, content.toString());
1430             }
1431         } catch (IOException e) {
1432             // This is expected to happen and is properly reported to the UI.
1433             throw e;
1434         } catch (CoreException e) {
1435             // This is expected to happen and is properly reported to the UI.
1436             throw e;
1437         } catch (Throwable t) {
1438             // Since we use some internal APIs, use a broad catch-all to report any
1439             // unexpected issue rather than crash the whole refactoring.
1440             status.addFatalError(
1441                     String.format("XML replace error: %1$s", t.getMessage()));
1442         } finally {
1443             if (smodel != null) {
1444                 smodel.releaseFromRead();
1445             }
1446         }
1447 
1448         return null;
1449     }
1450 
1451     /**
1452      * Computes the changes to be made to the source Android XML file and
1453      * returns a list of {@link Change}.
1454      * <p/>
1455      * This function scans an XML file, looking for an attribute value equals to
1456      * <code>tokenString</code>. If non null, <code>xmlAttrName</code> limit the search
1457      * to only attributes that have that name.
1458      * If found, a change is made to replace each occurrence of <code>tokenString</code>
1459      * by a new "@string/..." using the new <code>xmlStringId</code>.
1460      *
1461      * @param sourceFile The file to process.
1462      *          A status error will be generated if it does not exists.
1463      *          Must not be null.
1464      * @param tokenString The string to find. Must not be null or empty.
1465      * @param xmlAttrName Optional attribute name to limit the search. Can be null.
1466      * @param allConfigurations True if this function should can all XML files with the same
1467      *          name and the same resource type folder but with different configurations.
1468      * @param status Status used to report fatal errors.
1469      * @param monitor Used to log progress.
1470      */
1471     private List<Change> computeXmlSourceChanges(IFile sourceFile,
1472             String xmlStringId,
1473             String tokenString,
1474             String xmlAttrName,
1475             boolean allConfigurations,
1476             RefactoringStatus status,
1477             IProgressMonitor monitor) {
1478 
1479         if (!sourceFile.exists()) {
1480             status.addFatalError(String.format("XML file '%1$s' does not exist.",
1481                     sourceFile.getFullPath().toOSString()));
1482             return null;
1483         }
1484 
1485         // We shouldn't be trying to replace a null or empty string.
1486         assert tokenString != null && tokenString.length() > 0;
1487         if (tokenString == null || tokenString.length() == 0) {
1488             return null;
1489         }
1490 
1491         // Note: initially this method was only processing files using a pattern
1492         //   /project/res/<type>-<configuration>/<filename.xml>
1493         // However the last version made that more generic to be able to process any XML
1494         // files. We should probably revisit and simplify this later.
1495         HashSet<IFile> files = new HashSet<IFile>();
1496         files.add(sourceFile);
1497 
1498         if (allConfigurations && SdkConstants.EXT_XML.equals(sourceFile.getFileExtension())) {
1499             IPath path = sourceFile.getFullPath();
1500             if (path.segmentCount() == 4 && path.segment(1).equals(SdkConstants.FD_RESOURCES)) {
1501                 IProject project = sourceFile.getProject();
1502                 String filename = path.segment(3);
1503                 String initialTypeName = path.segment(2);
1504                 ResourceFolderType type = ResourceFolderType.getFolderType(initialTypeName);
1505 
1506                 IContainer res = sourceFile.getParent().getParent();
1507                 if (type != null && res != null && res.getType() == IResource.FOLDER) {
1508                     try {
1509                         for (IResource r : res.members()) {
1510                             if (r != null && r.getType() == IResource.FOLDER) {
1511                                 String name = r.getName();
1512                                 // Skip the initial folder name, it's already in the list.
1513                                 if (!name.equals(initialTypeName)) {
1514                                     // Only accept the same folder type (e.g. layout-*)
1515                                     ResourceFolderType t =
1516                                         ResourceFolderType.getFolderType(name);
1517                                     if (type.equals(t)) {
1518                                         // recompute the path
1519                                         IPath p = res.getProjectRelativePath().append(name).
1520                                                                                append(filename);
1521                                         IResource f = project.findMember(p);
1522                                         if (f != null && f instanceof IFile) {
1523                                             files.add((IFile) f);
1524                                         }
1525                                     }
1526                                 }
1527                             }
1528                         }
1529                     } catch (CoreException e) {
1530                         // Ignore.
1531                     }
1532                 }
1533             }
1534         }
1535 
1536         SubMonitor subMonitor = SubMonitor.convert(monitor, Math.min(1, files.size()));
1537 
1538         ArrayList<Change> changes = new ArrayList<Change>();
1539 
1540         // Portability note: getModelManager is part of wst.sse.core however the
1541         // interface returned is part of wst.sse.core.internal.provisional so we can
1542         // expect it to change in a distant future if they start cleaning their codebase,
1543         // however unlikely that is.
1544         IModelManager modelManager = StructuredModelManager.getModelManager();
1545 
1546         for (IFile file : files) {
1547 
1548             IStructuredModel smodel = null;
1549             MultiTextEdit multiEdit = null;
1550             TextFileChange xmlChange = null;
1551             ArrayList<TextEditGroup> editGroups = null;
1552 
1553             try {
1554                 IStructuredDocument sdoc = null;
1555 
1556                 smodel = modelManager.getExistingModelForRead(file);
1557                 if (smodel != null) {
1558                     sdoc = smodel.getStructuredDocument();
1559                 } else if (smodel == null) {
1560                     // The model is not currently open.
1561                     if (file.exists()) {
1562                         sdoc = modelManager.createStructuredDocumentFor(file);
1563                     } else {
1564                         sdoc = modelManager.createNewStructuredDocumentFor(file);
1565                     }
1566                 }
1567 
1568                 if (sdoc == null) {
1569                     status.addFatalError("XML structured document not found");     //$NON-NLS-1$
1570                     continue;
1571                 }
1572 
1573                 multiEdit = new MultiTextEdit();
1574                 editGroups = new ArrayList<TextEditGroup>();
1575                 xmlChange = new TextFileChange(getName(), file);
1576                 xmlChange.setTextType("xml");   //$NON-NLS-1$
1577 
1578                 String quotedReplacement = quotedAttrValue(STRING_PREFIX + xmlStringId);
1579 
1580                 // Prepare the change set
1581                 for (IStructuredDocumentRegion regions : sdoc.getStructuredDocumentRegions()) {
1582                     // Only look at XML "top regions"
1583                     if (!DOMRegionContext.XML_TAG_NAME.equals(regions.getType())) {
1584                         continue;
1585                     }
1586 
1587                     int nb = regions.getNumberOfRegions();
1588                     ITextRegionList list = regions.getRegions();
1589                     String lastAttrName = null;
1590 
1591                     for (int i = 0; i < nb; i++) {
1592                         ITextRegion subRegion = list.get(i);
1593                         String type = subRegion.getType();
1594 
1595                         if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
1596                             // Memorize the last attribute name seen
1597                             lastAttrName = regions.getText(subRegion);
1598 
1599                         } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
1600                             // Check this is the attribute and the original string
1601                             String text = regions.getText(subRegion);
1602 
1603                             // Remove " or ' quoting present in the attribute value
1604                             text = unquoteAttrValue(text);
1605 
1606                             if (tokenString.equals(text) &&
1607                                     (xmlAttrName == null || xmlAttrName.equals(lastAttrName))) {
1608 
1609                                 // Found an occurrence. Create a change for it.
1610                                 TextEdit edit = new ReplaceEdit(
1611                                         regions.getStartOffset() + subRegion.getStart(),
1612                                         subRegion.getTextLength(),
1613                                         quotedReplacement);
1614                                 TextEditGroup editGroup = new TextEditGroup(
1615                                         "Replace attribute string by ID",
1616                                         edit);
1617 
1618                                 multiEdit.addChild(edit);
1619                                 editGroups.add(editGroup);
1620                             }
1621                         }
1622                     }
1623                 }
1624             } catch (Throwable t) {
1625                 // Since we use some internal APIs, use a broad catch-all to report any
1626                 // unexpected issue rather than crash the whole refactoring.
1627                 status.addFatalError(
1628                         String.format("XML refactoring error: %1$s", t.getMessage()));
1629             } finally {
1630                 if (smodel != null) {
1631                     smodel.releaseFromRead();
1632                 }
1633 
1634                 if (multiEdit != null &&
1635                         xmlChange != null &&
1636                         editGroups != null &&
1637                         multiEdit.hasChildren()) {
1638                     xmlChange.setEdit(multiEdit);
1639                     for (TextEditGroup group : editGroups) {
1640                         xmlChange.addTextEditChangeGroup(
1641                                 new TextEditChangeGroup(xmlChange, group));
1642                     }
1643                     changes.add(xmlChange);
1644                 }
1645                 subMonitor.worked(1);
1646             }
1647         } // for files
1648 
1649         if (changes.size() > 0) {
1650             return changes;
1651         }
1652         return null;
1653     }
1654 
1655     /**
1656      * Returns a quoted attribute value suitable to be placed after an attributeName=
1657      * statement in an XML stream.
1658      *
1659      * According to http://www.w3.org/TR/2008/REC-xml-20081126/#NT-AttValue
1660      * the attribute value can be either quoted using ' or " and the corresponding
1661      * entities &apos; or &quot; must be used inside.
1662      */
1663     private String quotedAttrValue(String attrValue) {
1664         if (attrValue.indexOf('"') == -1) {
1665             // no double-quotes inside, use double-quotes around.
1666             return '"' + attrValue + '"';
1667         }
1668         if (attrValue.indexOf('\'') == -1) {
1669             // no single-quotes inside, use single-quotes around.
1670             return '\'' + attrValue + '\'';
1671         }
1672         // If we get here, there's a mix. Opt for double-quote around and replace
1673         // inner double-quotes.
1674         attrValue = attrValue.replace("\"", QUOT_ENTITY);  //$NON-NLS-1$
1675         return '"' + attrValue + '"';
1676     }
1677 
1678     // --- Java changes ---
1679 
1680     /**
1681      * Returns a foreach compatible iterator over all ICompilationUnit in the project.
1682      */
1683     private Iterable<ICompilationUnit> findAllJavaUnits() {
1684         final IJavaProject javaProject = JavaCore.create(mProject);
1685 
1686         return new Iterable<ICompilationUnit>() {
1687             @Override
1688             public Iterator<ICompilationUnit> iterator() {
1689                 return new Iterator<ICompilationUnit>() {
1690                     final Queue<ICompilationUnit> mUnits = new LinkedList<ICompilationUnit>();
1691                     final Queue<IPackageFragment> mFragments = new LinkedList<IPackageFragment>();
1692                     {
1693                         try {
1694                             IPackageFragment[] tmpFrags = javaProject.getPackageFragments();
1695                             if (tmpFrags != null && tmpFrags.length > 0) {
1696                                 mFragments.addAll(Arrays.asList(tmpFrags));
1697                             }
1698                         } catch (JavaModelException e) {
1699                             // pass
1700                         }
1701                     }
1702 
1703                     @Override
1704                     public boolean hasNext() {
1705                         if (!mUnits.isEmpty()) {
1706                             return true;
1707                         }
1708 
1709                         while (!mFragments.isEmpty()) {
1710                             try {
1711                                 IPackageFragment fragment = mFragments.poll();
1712                                 if (fragment.getKind() == IPackageFragmentRoot.K_SOURCE) {
1713                                     ICompilationUnit[] tmpUnits = fragment.getCompilationUnits();
1714                                     if (tmpUnits != null && tmpUnits.length > 0) {
1715                                         mUnits.addAll(Arrays.asList(tmpUnits));
1716                                         return true;
1717                                     }
1718                                 }
1719                             } catch (JavaModelException e) {
1720                                 // pass
1721                             }
1722                         }
1723                         return false;
1724                     }
1725 
1726                     @Override
1727                     public ICompilationUnit next() {
1728                         ICompilationUnit unit = mUnits.poll();
1729                         hasNext();
1730                         return unit;
1731                     }
1732 
1733                     @Override
1734                     public void remove() {
1735                         throw new UnsupportedOperationException(
1736                                 "This iterator does not support removal");  //$NON-NLS-1$
1737                     }
1738                 };
1739             }
1740         };
1741     }
1742 
1743     /**
1744      * Computes the changes to be made to Java file(s) and returns a list of {@link Change}.
1745      * <p/>
1746      * This function scans a Java compilation unit using {@link ReplaceStringsVisitor}, looking
1747      * for a string literal equals to <code>tokenString</code>.
1748      * If found, a change is made to replace each occurrence of <code>tokenString</code> by
1749      * a piece of Java code that somehow accesses R.string.<code>xmlStringId</code>.
1750      *
1751      * @param unit The compilated unit to process. Must not be null.
1752      * @param tokenString The string to find. Must not be null or empty.
1753      * @param status Status used to report fatal errors.
1754      * @param monitor Used to log progress.
1755      */
1756     private List<Change> computeJavaChanges(ICompilationUnit unit,
1757             String xmlStringId,
1758             String tokenString,
1759             RefactoringStatus status,
1760             SubMonitor monitor) {
1761 
1762         // We shouldn't be trying to replace a null or empty string.
1763         assert tokenString != null && tokenString.length() > 0;
1764         if (tokenString == null || tokenString.length() == 0) {
1765             return null;
1766         }
1767 
1768         // Get the Android package name from the Android Manifest. We need it to create
1769         // the FQCN of the R class.
1770         String packageName = null;
1771         String error = null;
1772         IResource manifestFile = mProject.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML);
1773         if (manifestFile == null || manifestFile.getType() != IResource.FILE) {
1774             error = "File not found";
1775         } else {
1776             ManifestData manifestData = AndroidManifestHelper.parseForData((IFile) manifestFile);
1777             if (manifestData == null) {
1778                 error = "Invalid content";
1779             } else {
1780                 packageName = manifestData.getPackage();
1781                 if (packageName == null) {
1782                     error = "Missing package definition";
1783                 }
1784             }
1785         }
1786 
1787         if (error != null) {
1788             status.addFatalError(
1789                     String.format("Failed to parse file %1$s: %2$s.",
1790                             manifestFile == null ? "" : manifestFile.getFullPath(),  //$NON-NLS-1$
1791                             error));
1792             return null;
1793         }
1794 
1795         // Right now the changes array will contain one TextFileChange at most.
1796         ArrayList<Change> changes = new ArrayList<Change>();
1797 
1798         // This is the unit that will be modified.
1799         TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource());
1800         change.setTextType("java"); //$NON-NLS-1$
1801 
1802         // Create an AST for this compilation unit
1803         ASTParser parser = ASTParser.newParser(AST.JLS3);
1804         parser.setProject(unit.getJavaProject());
1805         parser.setSource(unit);
1806         parser.setResolveBindings(true);
1807         ASTNode node = parser.createAST(monitor.newChild(1));
1808 
1809         // The ASTNode must be a CompilationUnit, by design
1810         if (!(node instanceof CompilationUnit)) {
1811             status.addFatalError(String.format("Internal error: ASTNode class %s",  //$NON-NLS-1$
1812                     node.getClass()));
1813             return null;
1814         }
1815 
1816         // ImportRewrite will allow us to add the new type to the imports and will resolve
1817         // what the Java source must reference, e.g. the FQCN or just the simple name.
1818         ImportRewrite importRewrite = ImportRewrite.create((CompilationUnit) node, true);
1819         String Rqualifier = packageName + ".R"; //$NON-NLS-1$
1820         Rqualifier = importRewrite.addImport(Rqualifier);
1821 
1822         // Rewrite the AST itself via an ASTVisitor
1823         AST ast = node.getAST();
1824         ASTRewrite astRewrite = ASTRewrite.create(ast);
1825         ArrayList<TextEditGroup> astEditGroups = new ArrayList<TextEditGroup>();
1826         ReplaceStringsVisitor visitor = new ReplaceStringsVisitor(
1827                 ast, astRewrite, astEditGroups,
1828                 tokenString, Rqualifier, xmlStringId);
1829         node.accept(visitor);
1830 
1831         // Finally prepare the change set
1832         try {
1833             MultiTextEdit edit = new MultiTextEdit();
1834 
1835             // Create the edit to change the imports, only if anything changed
1836             TextEdit subEdit = importRewrite.rewriteImports(monitor.newChild(1));
1837             if (subEdit.hasChildren()) {
1838                 edit.addChild(subEdit);
1839             }
1840 
1841             // Create the edit to change the Java source, only if anything changed
1842             subEdit = astRewrite.rewriteAST();
1843             if (subEdit.hasChildren()) {
1844                 edit.addChild(subEdit);
1845             }
1846 
1847             // Only create a change set if any edit was collected
1848             if (edit.hasChildren()) {
1849                 change.setEdit(edit);
1850 
1851                 // Create TextEditChangeGroups which let the user turn changes on or off
1852                 // individually. This must be done after the change.setEdit() call above.
1853                 for (TextEditGroup editGroup : astEditGroups) {
1854                     TextEditChangeGroup group = new TextEditChangeGroup(change, editGroup);
1855                     if (editGroup instanceof EnabledTextEditGroup) {
1856                         group.setEnabled(((EnabledTextEditGroup) editGroup).isEnabled());
1857                     }
1858                     change.addTextEditChangeGroup(group);
1859                 }
1860 
1861                 changes.add(change);
1862             }
1863 
1864             monitor.worked(1);
1865 
1866             if (changes.size() > 0) {
1867                 return changes;
1868             }
1869 
1870         } catch (CoreException e) {
1871             // ImportRewrite.rewriteImports failed.
1872             status.addFatalError(e.getMessage());
1873         }
1874         return null;
1875     }
1876 
1877     // ----
1878 
1879     /**
1880      * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the
1881      * work and creates a descriptor that can be used to replay that refactoring later.
1882      *
1883      * @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)
1884      *
1885      * @throws CoreException
1886      */
1887     @Override
1888     public Change createChange(IProgressMonitor monitor)
1889             throws CoreException, OperationCanceledException {
1890 
1891         try {
1892             monitor.beginTask("Applying changes...", 1);
1893 
1894             CompositeChange change = new CompositeChange(
1895                     getName(),
1896                     mChanges.toArray(new Change[mChanges.size()])) {
1897                 @Override
1898                 public ChangeDescriptor getDescriptor() {
1899 
1900                     String comment = String.format(
1901                             "Extracts string '%1$s' into R.string.%2$s",
1902                             mTokenString,
1903                             mXmlStringId);
1904 
1905                     ExtractStringDescriptor desc = new ExtractStringDescriptor(
1906                             mProject.getName(), //project
1907                             comment, //description
1908                             comment, //comment
1909                             createArgumentMap());
1910 
1911                     return new RefactoringChangeDescriptor(desc);
1912                 }
1913             };
1914 
1915             monitor.worked(1);
1916 
1917             return change;
1918 
1919         } finally {
1920             monitor.done();
1921         }
1922 
1923     }
1924 
1925     /**
1926      * Given a file project path, returns its resource in the same project than the
1927      * compilation unit. The resource may not exist.
1928      */
1929     private IResource getTargetXmlResource(String xmlFileWsPath) {
1930         IResource resource = mProject.getFile(xmlFileWsPath);
1931         return resource;
1932     }
1933 }
1934