• 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.uimodel.UiAttributeNode;
26 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
27 import com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors;
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             @Override
1003             public Iterator<IFile> iterator() {
1004                 return new Iterator<IFile>() {
1005                     final Queue<IFile> mFiles = new LinkedList<IFile>();
1006                     final Queue<IResource> mFolders = new LinkedList<IResource>();
1007                     IPath mFilterPath1 = null;
1008                     IPath mFilterPath2 = null;
1009                     {
1010                         // Filter out the XML file where we'll be writing the XML string id.
1011                         IResource filterRes = mProject.findMember(mTargetXmlFileWsPath);
1012                         if (filterRes != null) {
1013                             mFilterPath1 = filterRes.getFullPath();
1014                         }
1015                         // Filter out the XML source file, if any (e.g. typically a layout)
1016                         if (mFile != null) {
1017                             mFilterPath2 = mFile.getFullPath();
1018                         }
1019 
1020                         // We want to process the manifest
1021                         IResource man = mProject.findMember("AndroidManifest.xml"); // TODO find a constant
1022                         if (man.exists() && man instanceof IFile && !man.equals(mFile)) {
1023                             mFiles.add((IFile) man);
1024                         }
1025 
1026                         // Add all /res folders (technically we don't need to process /res/values
1027                         // XML files that contain resources/string elements, but it's easier to
1028                         // not filter them out.)
1029                         IFolder f = mProject.getFolder(AdtConstants.WS_RESOURCES);
1030                         if (f.exists()) {
1031                             try {
1032                                 mFolders.addAll(
1033                                         Arrays.asList(f.members(IContainer.EXCLUDE_DERIVED)));
1034                             } catch (CoreException e) {
1035                                 // pass
1036                             }
1037                         }
1038                     }
1039 
1040                     @Override
1041                     public boolean hasNext() {
1042                         if (!mFiles.isEmpty()) {
1043                             return true;
1044                         }
1045 
1046                         while (!mFolders.isEmpty()) {
1047                             IResource res = mFolders.poll();
1048                             if (res.exists() && res instanceof IFolder) {
1049                                 IFolder f = (IFolder) res;
1050                                 try {
1051                                     getFileList(f);
1052                                     if (!mFiles.isEmpty()) {
1053                                         return true;
1054                                     }
1055                                 } catch (CoreException e) {
1056                                     // pass
1057                                 }
1058                             }
1059                         }
1060                         return false;
1061                     }
1062 
1063                     private void getFileList(IFolder folder) throws CoreException {
1064                         for (IResource res : folder.members(IContainer.EXCLUDE_DERIVED)) {
1065                             // Only accept file resources which are not derived and actually exist
1066                             if (res.exists() && !res.isDerived() && res instanceof IFile) {
1067                                 IFile file = (IFile) res;
1068                                 // Must have an XML extension
1069                                 if (AdtConstants.EXT_XML.equals(file.getFileExtension())) {
1070                                     IPath p = file.getFullPath();
1071                                     // And not be either paths we want to filter out
1072                                     if ((mFilterPath1 != null && mFilterPath1.equals(p)) ||
1073                                             (mFilterPath2 != null && mFilterPath2.equals(p))) {
1074                                         continue;
1075                                     }
1076                                     mFiles.add(file);
1077                                 }
1078                             }
1079                         }
1080                     }
1081 
1082                     @Override
1083                     public IFile next() {
1084                         IFile file = mFiles.poll();
1085                         hasNext();
1086                         return file;
1087                     }
1088 
1089                     @Override
1090                     public void remove() {
1091                         throw new UnsupportedOperationException(
1092                             "This iterator does not support removal");  //$NON-NLS-1$
1093                     }
1094                 };
1095             }
1096         };
1097     }
1098 
1099     /**
1100      * Internal helper that actually prepares the {@link Change} that adds the given
1101      * ID to the given XML File.
1102      * <p/>
1103      * This does not actually modify the file.
1104      *
1105      * @param targetXml The file resource to modify.
1106      * @param xmlStringId The new ID to insert.
1107      * @param tokenString The old string, which will be the value in the XML string.
1108      * @return A new {@link TextEdit} that describes how to change the file.
1109      */
1110     private Change createXmlChanges(IFile targetXml,
1111             String xmlStringId,
1112             String tokenString,
1113             RefactoringStatus status,
1114             SubMonitor monitor) {
1115 
1116         TextFileChange xmlChange = new TextFileChange(getName(), targetXml);
1117         xmlChange.setTextType(AdtConstants.EXT_XML);
1118 
1119         String error = "";                  //$NON-NLS-1$
1120         TextEdit edit = null;
1121         TextEditGroup editGroup = null;
1122 
1123         try {
1124             if (!targetXml.exists()) {
1125                 // Kludge: use targetXml==null as a signal this is a new file being created
1126                 targetXml = null;
1127             }
1128 
1129             edit = createXmlReplaceEdit(targetXml, xmlStringId, tokenString, status,
1130                     SubMonitor.convert(monitor, 1));
1131         } catch (IOException e) {
1132             error = e.toString();
1133         } catch (CoreException e) {
1134             // Failed to read file. Ignore. Will handle error below.
1135             error = e.toString();
1136         }
1137 
1138         if (edit == null) {
1139             status.addFatalError(String.format("Failed to modify file %1$s%2$s",
1140                     targetXml == null ? "" : targetXml.getFullPath(),   //$NON-NLS-1$
1141                     error == null ? "" : ": " + error));                //$NON-NLS-1$
1142             return null;
1143         }
1144 
1145         editGroup = new TextEditGroup(targetXml == null ? "Create <string> in new XML file"
1146                                                         : "Insert <string> in XML file",
1147                                       edit);
1148 
1149         xmlChange.setEdit(edit);
1150         // The TextEditChangeGroup let the user toggle this change on and off later.
1151         xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup));
1152 
1153         monitor.worked(1);
1154         return xmlChange;
1155     }
1156 
1157     /**
1158      * Scan the XML file to find the best place where to insert the new string element.
1159      * <p/>
1160      * This handles a variety of cases, including replacing existing ids in place,
1161      * adding the top resources element if missing and the XML PI if not present.
1162      * It tries to preserve indentation when adding new elements at the end of an existing XML.
1163      *
1164      * @param file The XML file to modify, that must be present in the workspace.
1165      *             Pass null to create a change for a new file that doesn't exist yet.
1166      * @param xmlStringId The new ID to insert.
1167      * @param tokenString The old string, which will be the value in the XML string.
1168      * @param status The in-out refactoring status. Used to log a more detailed error if the
1169      *          XML has a top element that is not a resources element.
1170      * @param monitor A monitor to track progress.
1171      * @return A new {@link TextEdit} for either a replace or an insert operation, or null in case
1172      *          of error.
1173      * @throws CoreException - if the file's contents or description can not be read.
1174      * @throws IOException   - if the file's contents can not be read or its detected encoding does
1175      *                         not support its contents.
1176      */
1177     private TextEdit createXmlReplaceEdit(IFile file,
1178             String xmlStringId,
1179             String tokenString,
1180             RefactoringStatus status,
1181             SubMonitor monitor)
1182                 throws IOException, CoreException {
1183 
1184         IModelManager modelMan = StructuredModelManager.getModelManager();
1185 
1186         final String NODE_RESOURCES = ValuesDescriptors.ROOT_ELEMENT;
1187         final String NODE_STRING = "string";    //$NON-NLS-1$ //TODO find or create constant
1188         final String ATTR_NAME = "name";        //$NON-NLS-1$ //TODO find or create constant
1189 
1190 
1191         // Scan the source to find the best insertion point.
1192 
1193         // 1- The most common case we need to handle is the one of inserting at the end
1194         //    of a valid XML document, respecting the whitespace last used.
1195         //
1196         // Ideally we have this structure:
1197         // <xml ...>
1198         // <resource>
1199         // ...ws1...<string>blah</string>...ws2...
1200         // </resource>
1201         //
1202         // where ws1 and ws2 are the whitespace respectively before and after the last element
1203         // just before the closing </resource>.
1204         // In this case we want to generate the new string just before ws2...</resource> with
1205         // the same whitespace as ws1.
1206         //
1207         // 2- Another expected case is there's already an existing string which "name" attribute
1208         //    equals to xmlStringId and we just want to replace its value.
1209         //
1210         // Other cases we need to handle:
1211         // 3- There is no element at all -> create a full new <resource>+<string> content.
1212         // 4- There is <resource/>, that is the tag is not opened. This can be handled as the
1213         //    previous case, generating full content but also replacing <resource/>.
1214         // 5- There is a top element that is not <resource>. That's a fatal error and we abort.
1215 
1216         IStructuredModel smodel = null;
1217 
1218         // Single and double quotes must be escaped in the <string>value</string> declaration
1219         tokenString = escapeString(tokenString);
1220 
1221         try {
1222             IStructuredDocument sdoc = null;
1223             boolean checkTopElement = true;
1224             boolean replaceStringContent = false;
1225             boolean hasPiXml = false;
1226             int newResStart = 0;
1227             int newResLength = 0;
1228             String lineSep = "\n";                  //$NON-NLS-1$
1229 
1230             if (file != null) {
1231                 smodel = modelMan.getExistingModelForRead(file);
1232                 if (smodel != null) {
1233                     sdoc = smodel.getStructuredDocument();
1234                 } else if (smodel == null) {
1235                     // The model is not currently open.
1236                     if (file.exists()) {
1237                         sdoc = modelMan.createStructuredDocumentFor(file);
1238                     } else {
1239                         sdoc = modelMan.createNewStructuredDocumentFor(file);
1240                     }
1241                 }
1242             }
1243 
1244             if (sdoc == null && file != null) {
1245                 // Get a document matching the actual saved file
1246                 sdoc = modelMan.createStructuredDocumentFor(file);
1247             }
1248 
1249             if (sdoc != null) {
1250                 String wsBefore = "";   //$NON-NLS-1$
1251                 String lastWs = null;
1252 
1253                 lineSep = sdoc.getLineDelimiter();
1254                 if (lineSep == null || lineSep.length() == 0) {
1255                     // That wasn't too useful, let's go back to a reasonable default
1256                     lineSep = "\n"; //$NON-NLS-1$
1257                 }
1258 
1259                 for (IStructuredDocumentRegion regions : sdoc.getStructuredDocumentRegions()) {
1260                     String type = regions.getType();
1261 
1262                     if (DOMRegionContext.XML_CONTENT.equals(type)) {
1263 
1264                         if (replaceStringContent) {
1265                             // Generate a replacement for a <string> value matching the string ID.
1266                             return new ReplaceEdit(
1267                                     regions.getStartOffset(), regions.getLength(), tokenString);
1268                         }
1269 
1270                         // Otherwise capture what should be whitespace content
1271                         lastWs = regions.getFullText();
1272                         continue;
1273 
1274                     } else if (DOMRegionContext.XML_PI_OPEN.equals(type) && !hasPiXml) {
1275 
1276                         int nb = regions.getNumberOfRegions();
1277                         ITextRegionList list = regions.getRegions();
1278                         for (int i = 0; i < nb; i++) {
1279                             ITextRegion region = list.get(i);
1280                             type = region.getType();
1281                             if (DOMRegionContext.XML_TAG_NAME.equals(type)) {
1282                                 String name = regions.getText(region);
1283                                 if ("xml".equals(name)) {   //$NON-NLS-1$
1284                                     hasPiXml = true;
1285                                     break;
1286                                 }
1287                             }
1288                         }
1289                         continue;
1290 
1291                     } else if (!DOMRegionContext.XML_TAG_NAME.equals(type)) {
1292                         // ignore things which are not a tag nor text content (such as comments)
1293                         continue;
1294                     }
1295 
1296                     int nb = regions.getNumberOfRegions();
1297                     ITextRegionList list = regions.getRegions();
1298 
1299                     String name = null;
1300                     String attrName = null;
1301                     String attrValue = null;
1302                     boolean isEmptyTag = false;
1303                     boolean isCloseTag = false;
1304 
1305                     for (int i = 0; i < nb; i++) {
1306                         ITextRegion region = list.get(i);
1307                         type = region.getType();
1308 
1309                         if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) {
1310                             isCloseTag = true;
1311                         } else if (DOMRegionContext.XML_EMPTY_TAG_CLOSE.equals(type)) {
1312                             isEmptyTag = true;
1313                         } else if (DOMRegionContext.XML_TAG_NAME.equals(type)) {
1314                             name = regions.getText(region);
1315                         } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type) &&
1316                                 NODE_STRING.equals(name)) {
1317                             // Record the attribute names into a <string> element.
1318                             attrName = regions.getText(region);
1319                         } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type) &&
1320                                 ATTR_NAME.equals(attrName)) {
1321                             // Record the value of a <string name=...> attribute
1322                             attrValue = regions.getText(region);
1323 
1324                             if (attrValue != null &&
1325                                     unquoteAttrValue(attrValue).equals(xmlStringId)) {
1326                                 // We found a <string name=> matching the string ID to replace.
1327                                 // We'll generate a replacement when we process the string value
1328                                 // (that is the next XML_CONTENT region.)
1329                                 replaceStringContent = true;
1330                             }
1331                         }
1332                     }
1333 
1334                     if (checkTopElement) {
1335                         // Check the top element has a resource name
1336                         checkTopElement = false;
1337                         if (!NODE_RESOURCES.equals(name)) {
1338                             status.addFatalError(
1339                                     String.format("XML file lacks a <resource> tag: %1$s",
1340                                             mTargetXmlFileWsPath));
1341                             return null;
1342 
1343                         }
1344 
1345                         if (isEmptyTag) {
1346                             // The top element is an empty "<resource/>" tag. We need to do
1347                             // a full new resource+string replacement.
1348                             newResStart = regions.getStartOffset();
1349                             newResLength = regions.getLength();
1350                         }
1351                     }
1352 
1353                     if (NODE_RESOURCES.equals(name)) {
1354                         if (isCloseTag) {
1355                             // We found the </resource> tag and we want
1356                             // to insert just before this one.
1357 
1358                             StringBuilder content = new StringBuilder();
1359                             content.append(wsBefore)
1360                                    .append("<string name=\"")                   //$NON-NLS-1$
1361                                    .append(xmlStringId)
1362                                    .append("\">")                               //$NON-NLS-1$
1363                                    .append(tokenString)
1364                                    .append("</string>");                        //$NON-NLS-1$
1365 
1366                             // Backup to insert before the whitespace preceding </resource>
1367                             IStructuredDocumentRegion insertBeforeReg = regions;
1368                             while (true) {
1369                                 IStructuredDocumentRegion previous = insertBeforeReg.getPrevious();
1370                                 if (previous != null &&
1371                                         DOMRegionContext.XML_CONTENT.equals(previous.getType()) &&
1372                                         previous.getText().trim().length() == 0) {
1373                                     insertBeforeReg = previous;
1374                                 } else {
1375                                     break;
1376                                 }
1377                             }
1378                             if (insertBeforeReg == regions) {
1379                                 // If we have not found any whitespace before </resources>,
1380                                 // at least add a line separator.
1381                                 content.append(lineSep);
1382                             }
1383 
1384                             return new InsertEdit(insertBeforeReg.getStartOffset(),
1385                                                   content.toString());
1386                         }
1387                     } else {
1388                         // For any other tag than <resource>, capture whitespace before and after.
1389                         if (!isCloseTag) {
1390                             wsBefore = lastWs;
1391                         }
1392                     }
1393                 }
1394             }
1395 
1396             // We reach here either because there's no XML content at all or because
1397             // there's an empty <resource/>.
1398             // Provide a full new resource+string replacement.
1399             StringBuilder content = new StringBuilder();
1400             if (!hasPiXml) {
1401                 content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); //$NON-NLS-1$
1402                 content.append(lineSep);
1403             } else if (newResLength == 0 && sdoc != null) {
1404                 // If inserting at the end, check if the last region is some whitespace.
1405                 // If there's no newline, insert one ourselves.
1406                 IStructuredDocumentRegion lastReg = sdoc.getLastStructuredDocumentRegion();
1407                 if (lastReg != null && lastReg.getText().indexOf('\n') == -1) {
1408                     content.append('\n');
1409                 }
1410             }
1411 
1412             // FIXME how to access formatting preferences to generate the proper indentation?
1413             content.append("<resources>").append(lineSep);                  //$NON-NLS-1$
1414             content.append("    <string name=\"")                           //$NON-NLS-1$
1415                    .append(xmlStringId)
1416                    .append("\">")                                           //$NON-NLS-1$
1417                    .append(tokenString)
1418                    .append("</string>")                                     //$NON-NLS-1$
1419                    .append(lineSep);
1420             content.append("</resources>").append(lineSep);                 //$NON-NLS-1$
1421 
1422             if (newResLength > 0) {
1423                 // Replace existing piece
1424                 return new ReplaceEdit(newResStart, newResLength, content.toString());
1425             } else {
1426                 // Insert at the end.
1427                 int offset = sdoc == null ? 0 : sdoc.getLength();
1428                 return new InsertEdit(offset, content.toString());
1429             }
1430         } catch (IOException e) {
1431             // This is expected to happen and is properly reported to the UI.
1432             throw e;
1433         } catch (CoreException e) {
1434             // This is expected to happen and is properly reported to the UI.
1435             throw e;
1436         } catch (Throwable t) {
1437             // Since we use some internal APIs, use a broad catch-all to report any
1438             // unexpected issue rather than crash the whole refactoring.
1439             status.addFatalError(
1440                     String.format("XML replace error: %1$s", t.getMessage()));
1441         } finally {
1442             if (smodel != null) {
1443                 smodel.releaseFromRead();
1444             }
1445         }
1446 
1447         return null;
1448     }
1449 
1450     /**
1451      * Escape a string value to be placed in a string resource file such that it complies with
1452      * the escaping rules described here:
1453      *   http://developer.android.com/guide/topics/resources/string-resource.html
1454      * More examples of the escaping rules can be found here:
1455      *   http://androidcookbook.com/Recipe.seam?recipeId=2219&recipeFrom=ViewTOC
1456      * This method assumes that the String is not escaped already.
1457      *
1458      * Rules:
1459      * <ul>
1460      * <li>Double quotes are needed if string starts or ends with at least one space.
1461      * <li>{@code @, ?} at beginning of string have to be escaped with a backslash.
1462      * <li>{@code ', ", \} have to be escaped with a backslash.
1463      * <li>{@code <, >, &} have to be replaced by their predefined xml entity.
1464      * <li>{@code \n, \t} have to be replaced by a backslash and the appropriate character.
1465      * </ul>
1466      * @param s the string to be escaped
1467      * @return the escaped string as it would appear in the XML text in a values file
1468      */
1469     public static String escapeString(String s) {
1470         int n = s.length();
1471         if (n == 0) {
1472             return "";
1473         }
1474 
1475         StringBuilder sb = new StringBuilder(s.length() * 2);
1476         boolean hasSpace = s.charAt(0) == ' ' || s.charAt(n - 1) == ' ';
1477 
1478         if (hasSpace) {
1479             sb.append('"');
1480         } else if (s.charAt(0) == '@' || s.charAt(0) == '?') {
1481             sb.append('\\');
1482         }
1483 
1484         for (int i = 0; i < n; ++i) {
1485             char c = s.charAt(i);
1486             switch (c) {
1487                 case '\'':
1488                     if (!hasSpace) {
1489                         sb.append('\\');
1490                     }
1491                     sb.append(c);
1492                     break;
1493                 case '"':
1494                 case '\\':
1495                     sb.append('\\');
1496                     sb.append(c);
1497                     break;
1498                 case '<':
1499                     sb.append("&lt;"); //$NON-NLS-1$
1500                     break;
1501                 case '&':
1502                     sb.append("&amp;"); //$NON-NLS-1$
1503                     break;
1504                 case '\n':
1505                     sb.append("\\n"); //$NON-NLS-1$
1506                     break;
1507                 case '\t':
1508                     sb.append("\\t"); //$NON-NLS-1$
1509                     break;
1510                 default:
1511                     sb.append(c);
1512                     break;
1513             }
1514         }
1515 
1516         if (hasSpace) {
1517             sb.append('"');
1518         }
1519 
1520         return sb.toString();
1521     }
1522 
1523     /**
1524      * Computes the changes to be made to the source Android XML file and
1525      * returns a list of {@link Change}.
1526      * <p/>
1527      * This function scans an XML file, looking for an attribute value equals to
1528      * <code>tokenString</code>. If non null, <code>xmlAttrName</code> limit the search
1529      * to only attributes that have that name.
1530      * If found, a change is made to replace each occurrence of <code>tokenString</code>
1531      * by a new "@string/..." using the new <code>xmlStringId</code>.
1532      *
1533      * @param sourceFile The file to process.
1534      *          A status error will be generated if it does not exists.
1535      *          Must not be null.
1536      * @param tokenString The string to find. Must not be null or empty.
1537      * @param xmlAttrName Optional attribute name to limit the search. Can be null.
1538      * @param allConfigurations True if this function should can all XML files with the same
1539      *          name and the same resource type folder but with different configurations.
1540      * @param status Status used to report fatal errors.
1541      * @param monitor Used to log progress.
1542      */
1543     private List<Change> computeXmlSourceChanges(IFile sourceFile,
1544             String xmlStringId,
1545             String tokenString,
1546             String xmlAttrName,
1547             boolean allConfigurations,
1548             RefactoringStatus status,
1549             IProgressMonitor monitor) {
1550 
1551         if (!sourceFile.exists()) {
1552             status.addFatalError(String.format("XML file '%1$s' does not exist.",
1553                     sourceFile.getFullPath().toOSString()));
1554             return null;
1555         }
1556 
1557         // We shouldn't be trying to replace a null or empty string.
1558         assert tokenString != null && tokenString.length() > 0;
1559         if (tokenString == null || tokenString.length() == 0) {
1560             return null;
1561         }
1562 
1563         // Note: initially this method was only processing files using a pattern
1564         //   /project/res/<type>-<configuration>/<filename.xml>
1565         // However the last version made that more generic to be able to process any XML
1566         // files. We should probably revisit and simplify this later.
1567         HashSet<IFile> files = new HashSet<IFile>();
1568         files.add(sourceFile);
1569 
1570         if (allConfigurations && AdtConstants.EXT_XML.equals(sourceFile.getFileExtension())) {
1571             IPath path = sourceFile.getFullPath();
1572             if (path.segmentCount() == 4 && path.segment(1).equals(SdkConstants.FD_RESOURCES)) {
1573                 IProject project = sourceFile.getProject();
1574                 String filename = path.segment(3);
1575                 String initialTypeName = path.segment(2);
1576                 ResourceFolderType type = ResourceFolderType.getFolderType(initialTypeName);
1577 
1578                 IContainer res = sourceFile.getParent().getParent();
1579                 if (type != null && res != null && res.getType() == IResource.FOLDER) {
1580                     try {
1581                         for (IResource r : res.members()) {
1582                             if (r != null && r.getType() == IResource.FOLDER) {
1583                                 String name = r.getName();
1584                                 // Skip the initial folder name, it's already in the list.
1585                                 if (!name.equals(initialTypeName)) {
1586                                     // Only accept the same folder type (e.g. layout-*)
1587                                     ResourceFolderType t =
1588                                         ResourceFolderType.getFolderType(name);
1589                                     if (type.equals(t)) {
1590                                         // recompute the path
1591                                         IPath p = res.getProjectRelativePath().append(name).
1592                                                                                append(filename);
1593                                         IResource f = project.findMember(p);
1594                                         if (f != null && f instanceof IFile) {
1595                                             files.add((IFile) f);
1596                                         }
1597                                     }
1598                                 }
1599                             }
1600                         }
1601                     } catch (CoreException e) {
1602                         // Ignore.
1603                     }
1604                 }
1605             }
1606         }
1607 
1608         SubMonitor subMonitor = SubMonitor.convert(monitor, Math.min(1, files.size()));
1609 
1610         ArrayList<Change> changes = new ArrayList<Change>();
1611 
1612         // Portability note: getModelManager is part of wst.sse.core however the
1613         // interface returned is part of wst.sse.core.internal.provisional so we can
1614         // expect it to change in a distant future if they start cleaning their codebase,
1615         // however unlikely that is.
1616         IModelManager modelManager = StructuredModelManager.getModelManager();
1617 
1618         for (IFile file : files) {
1619 
1620             IStructuredModel smodel = null;
1621             MultiTextEdit multiEdit = null;
1622             TextFileChange xmlChange = null;
1623             ArrayList<TextEditGroup> editGroups = null;
1624 
1625             try {
1626                 IStructuredDocument sdoc = null;
1627 
1628                 smodel = modelManager.getExistingModelForRead(file);
1629                 if (smodel != null) {
1630                     sdoc = smodel.getStructuredDocument();
1631                 } else if (smodel == null) {
1632                     // The model is not currently open.
1633                     if (file.exists()) {
1634                         sdoc = modelManager.createStructuredDocumentFor(file);
1635                     } else {
1636                         sdoc = modelManager.createNewStructuredDocumentFor(file);
1637                     }
1638                 }
1639 
1640                 if (sdoc == null) {
1641                     status.addFatalError("XML structured document not found");     //$NON-NLS-1$
1642                     continue;
1643                 }
1644 
1645                 multiEdit = new MultiTextEdit();
1646                 editGroups = new ArrayList<TextEditGroup>();
1647                 xmlChange = new TextFileChange(getName(), file);
1648                 xmlChange.setTextType("xml");   //$NON-NLS-1$
1649 
1650                 String quotedReplacement = quotedAttrValue(STRING_PREFIX + xmlStringId);
1651 
1652                 // Prepare the change set
1653                 for (IStructuredDocumentRegion regions : sdoc.getStructuredDocumentRegions()) {
1654                     // Only look at XML "top regions"
1655                     if (!DOMRegionContext.XML_TAG_NAME.equals(regions.getType())) {
1656                         continue;
1657                     }
1658 
1659                     int nb = regions.getNumberOfRegions();
1660                     ITextRegionList list = regions.getRegions();
1661                     String lastAttrName = null;
1662 
1663                     for (int i = 0; i < nb; i++) {
1664                         ITextRegion subRegion = list.get(i);
1665                         String type = subRegion.getType();
1666 
1667                         if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
1668                             // Memorize the last attribute name seen
1669                             lastAttrName = regions.getText(subRegion);
1670 
1671                         } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
1672                             // Check this is the attribute and the original string
1673                             String text = regions.getText(subRegion);
1674 
1675                             // Remove " or ' quoting present in the attribute value
1676                             text = unquoteAttrValue(text);
1677 
1678                             if (tokenString.equals(text) &&
1679                                     (xmlAttrName == null || xmlAttrName.equals(lastAttrName))) {
1680 
1681                                 // Found an occurrence. Create a change for it.
1682                                 TextEdit edit = new ReplaceEdit(
1683                                         regions.getStartOffset() + subRegion.getStart(),
1684                                         subRegion.getTextLength(),
1685                                         quotedReplacement);
1686                                 TextEditGroup editGroup = new TextEditGroup(
1687                                         "Replace attribute string by ID",
1688                                         edit);
1689 
1690                                 multiEdit.addChild(edit);
1691                                 editGroups.add(editGroup);
1692                             }
1693                         }
1694                     }
1695                 }
1696             } catch (Throwable t) {
1697                 // Since we use some internal APIs, use a broad catch-all to report any
1698                 // unexpected issue rather than crash the whole refactoring.
1699                 status.addFatalError(
1700                         String.format("XML refactoring error: %1$s", t.getMessage()));
1701             } finally {
1702                 if (smodel != null) {
1703                     smodel.releaseFromRead();
1704                 }
1705 
1706                 if (multiEdit != null &&
1707                         xmlChange != null &&
1708                         editGroups != null &&
1709                         multiEdit.hasChildren()) {
1710                     xmlChange.setEdit(multiEdit);
1711                     for (TextEditGroup group : editGroups) {
1712                         xmlChange.addTextEditChangeGroup(
1713                                 new TextEditChangeGroup(xmlChange, group));
1714                     }
1715                     changes.add(xmlChange);
1716                 }
1717                 subMonitor.worked(1);
1718             }
1719         } // for files
1720 
1721         if (changes.size() > 0) {
1722             return changes;
1723         }
1724         return null;
1725     }
1726 
1727     /**
1728      * Returns a quoted attribute value suitable to be placed after an attributeName=
1729      * statement in an XML stream.
1730      *
1731      * According to http://www.w3.org/TR/2008/REC-xml-20081126/#NT-AttValue
1732      * the attribute value can be either quoted using ' or " and the corresponding
1733      * entities &apos; or &quot; must be used inside.
1734      */
1735     private String quotedAttrValue(String attrValue) {
1736         if (attrValue.indexOf('"') == -1) {
1737             // no double-quotes inside, use double-quotes around.
1738             return '"' + attrValue + '"';
1739         }
1740         if (attrValue.indexOf('\'') == -1) {
1741             // no single-quotes inside, use single-quotes around.
1742             return '\'' + attrValue + '\'';
1743         }
1744         // If we get here, there's a mix. Opt for double-quote around and replace
1745         // inner double-quotes.
1746         attrValue = attrValue.replace("\"", "&quot;");  //$NON-NLS-1$ //$NON-NLS-2$
1747         return '"' + attrValue + '"';
1748     }
1749 
1750     // --- Java changes ---
1751 
1752     /**
1753      * Returns a foreach compatible iterator over all ICompilationUnit in the project.
1754      */
1755     private Iterable<ICompilationUnit> findAllJavaUnits() {
1756         final IJavaProject javaProject = JavaCore.create(mProject);
1757 
1758         return new Iterable<ICompilationUnit>() {
1759             @Override
1760             public Iterator<ICompilationUnit> iterator() {
1761                 return new Iterator<ICompilationUnit>() {
1762                     final Queue<ICompilationUnit> mUnits = new LinkedList<ICompilationUnit>();
1763                     final Queue<IPackageFragment> mFragments = new LinkedList<IPackageFragment>();
1764                     {
1765                         try {
1766                             IPackageFragment[] tmpFrags = javaProject.getPackageFragments();
1767                             if (tmpFrags != null && tmpFrags.length > 0) {
1768                                 mFragments.addAll(Arrays.asList(tmpFrags));
1769                             }
1770                         } catch (JavaModelException e) {
1771                             // pass
1772                         }
1773                     }
1774 
1775                     @Override
1776                     public boolean hasNext() {
1777                         if (!mUnits.isEmpty()) {
1778                             return true;
1779                         }
1780 
1781                         while (!mFragments.isEmpty()) {
1782                             try {
1783                                 IPackageFragment fragment = mFragments.poll();
1784                                 if (fragment.getKind() == IPackageFragmentRoot.K_SOURCE) {
1785                                     ICompilationUnit[] tmpUnits = fragment.getCompilationUnits();
1786                                     if (tmpUnits != null && tmpUnits.length > 0) {
1787                                         mUnits.addAll(Arrays.asList(tmpUnits));
1788                                         return true;
1789                                     }
1790                                 }
1791                             } catch (JavaModelException e) {
1792                                 // pass
1793                             }
1794                         }
1795                         return false;
1796                     }
1797 
1798                     @Override
1799                     public ICompilationUnit next() {
1800                         ICompilationUnit unit = mUnits.poll();
1801                         hasNext();
1802                         return unit;
1803                     }
1804 
1805                     @Override
1806                     public void remove() {
1807                         throw new UnsupportedOperationException(
1808                                 "This iterator does not support removal");  //$NON-NLS-1$
1809                     }
1810                 };
1811             }
1812         };
1813     }
1814 
1815     /**
1816      * Computes the changes to be made to Java file(s) and returns a list of {@link Change}.
1817      * <p/>
1818      * This function scans a Java compilation unit using {@link ReplaceStringsVisitor}, looking
1819      * for a string literal equals to <code>tokenString</code>.
1820      * If found, a change is made to replace each occurrence of <code>tokenString</code> by
1821      * a piece of Java code that somehow accesses R.string.<code>xmlStringId</code>.
1822      *
1823      * @param unit The compilated unit to process. Must not be null.
1824      * @param tokenString The string to find. Must not be null or empty.
1825      * @param status Status used to report fatal errors.
1826      * @param monitor Used to log progress.
1827      */
1828     private List<Change> computeJavaChanges(ICompilationUnit unit,
1829             String xmlStringId,
1830             String tokenString,
1831             RefactoringStatus status,
1832             SubMonitor monitor) {
1833 
1834         // We shouldn't be trying to replace a null or empty string.
1835         assert tokenString != null && tokenString.length() > 0;
1836         if (tokenString == null || tokenString.length() == 0) {
1837             return null;
1838         }
1839 
1840         // Get the Android package name from the Android Manifest. We need it to create
1841         // the FQCN of the R class.
1842         String packageName = null;
1843         String error = null;
1844         IResource manifestFile = mProject.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML);
1845         if (manifestFile == null || manifestFile.getType() != IResource.FILE) {
1846             error = "File not found";
1847         } else {
1848             ManifestData manifestData = AndroidManifestHelper.parseForData((IFile) manifestFile);
1849             if (manifestData == null) {
1850                 error = "Invalid content";
1851             } else {
1852                 packageName = manifestData.getPackage();
1853                 if (packageName == null) {
1854                     error = "Missing package definition";
1855                 }
1856             }
1857         }
1858 
1859         if (error != null) {
1860             status.addFatalError(
1861                     String.format("Failed to parse file %1$s: %2$s.",
1862                             manifestFile == null ? "" : manifestFile.getFullPath(),  //$NON-NLS-1$
1863                             error));
1864             return null;
1865         }
1866 
1867         // Right now the changes array will contain one TextFileChange at most.
1868         ArrayList<Change> changes = new ArrayList<Change>();
1869 
1870         // This is the unit that will be modified.
1871         TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource());
1872         change.setTextType("java"); //$NON-NLS-1$
1873 
1874         // Create an AST for this compilation unit
1875         ASTParser parser = ASTParser.newParser(AST.JLS3);
1876         parser.setProject(unit.getJavaProject());
1877         parser.setSource(unit);
1878         parser.setResolveBindings(true);
1879         ASTNode node = parser.createAST(monitor.newChild(1));
1880 
1881         // The ASTNode must be a CompilationUnit, by design
1882         if (!(node instanceof CompilationUnit)) {
1883             status.addFatalError(String.format("Internal error: ASTNode class %s",  //$NON-NLS-1$
1884                     node.getClass()));
1885             return null;
1886         }
1887 
1888         // ImportRewrite will allow us to add the new type to the imports and will resolve
1889         // what the Java source must reference, e.g. the FQCN or just the simple name.
1890         ImportRewrite importRewrite = ImportRewrite.create((CompilationUnit) node, true);
1891         String Rqualifier = packageName + ".R"; //$NON-NLS-1$
1892         Rqualifier = importRewrite.addImport(Rqualifier);
1893 
1894         // Rewrite the AST itself via an ASTVisitor
1895         AST ast = node.getAST();
1896         ASTRewrite astRewrite = ASTRewrite.create(ast);
1897         ArrayList<TextEditGroup> astEditGroups = new ArrayList<TextEditGroup>();
1898         ReplaceStringsVisitor visitor = new ReplaceStringsVisitor(
1899                 ast, astRewrite, astEditGroups,
1900                 tokenString, Rqualifier, xmlStringId);
1901         node.accept(visitor);
1902 
1903         // Finally prepare the change set
1904         try {
1905             MultiTextEdit edit = new MultiTextEdit();
1906 
1907             // Create the edit to change the imports, only if anything changed
1908             TextEdit subEdit = importRewrite.rewriteImports(monitor.newChild(1));
1909             if (subEdit.hasChildren()) {
1910                 edit.addChild(subEdit);
1911             }
1912 
1913             // Create the edit to change the Java source, only if anything changed
1914             subEdit = astRewrite.rewriteAST();
1915             if (subEdit.hasChildren()) {
1916                 edit.addChild(subEdit);
1917             }
1918 
1919             // Only create a change set if any edit was collected
1920             if (edit.hasChildren()) {
1921                 change.setEdit(edit);
1922 
1923                 // Create TextEditChangeGroups which let the user turn changes on or off
1924                 // individually. This must be done after the change.setEdit() call above.
1925                 for (TextEditGroup editGroup : astEditGroups) {
1926                     TextEditChangeGroup group = new TextEditChangeGroup(change, editGroup);
1927                     if (editGroup instanceof EnabledTextEditGroup) {
1928                         group.setEnabled(((EnabledTextEditGroup) editGroup).isEnabled());
1929                     }
1930                     change.addTextEditChangeGroup(group);
1931                 }
1932 
1933                 changes.add(change);
1934             }
1935 
1936             monitor.worked(1);
1937 
1938             if (changes.size() > 0) {
1939                 return changes;
1940             }
1941 
1942         } catch (CoreException e) {
1943             // ImportRewrite.rewriteImports failed.
1944             status.addFatalError(e.getMessage());
1945         }
1946         return null;
1947     }
1948 
1949     // ----
1950 
1951     /**
1952      * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the
1953      * work and creates a descriptor that can be used to replay that refactoring later.
1954      *
1955      * @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)
1956      *
1957      * @throws CoreException
1958      */
1959     @Override
1960     public Change createChange(IProgressMonitor monitor)
1961             throws CoreException, OperationCanceledException {
1962 
1963         try {
1964             monitor.beginTask("Applying changes...", 1);
1965 
1966             CompositeChange change = new CompositeChange(
1967                     getName(),
1968                     mChanges.toArray(new Change[mChanges.size()])) {
1969                 @Override
1970                 public ChangeDescriptor getDescriptor() {
1971 
1972                     String comment = String.format(
1973                             "Extracts string '%1$s' into R.string.%2$s",
1974                             mTokenString,
1975                             mXmlStringId);
1976 
1977                     ExtractStringDescriptor desc = new ExtractStringDescriptor(
1978                             mProject.getName(), //project
1979                             comment, //description
1980                             comment, //comment
1981                             createArgumentMap());
1982 
1983                     return new RefactoringChangeDescriptor(desc);
1984                 }
1985             };
1986 
1987             monitor.worked(1);
1988 
1989             return change;
1990 
1991         } finally {
1992             monitor.done();
1993         }
1994 
1995     }
1996 
1997     /**
1998      * Given a file project path, returns its resource in the same project than the
1999      * compilation unit. The resource may not exist.
2000      */
2001     private IResource getTargetXmlResource(String xmlFileWsPath) {
2002         IResource resource = mProject.getFile(xmlFileWsPath);
2003         return resource;
2004     }
2005 }
2006