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