• 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 com.android.ide.eclipse.adt.AndroidConstants;
20 import com.android.ide.eclipse.adt.internal.editors.AndroidEditor;
21 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
22 import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor;
23 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
24 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
25 import com.android.ide.eclipse.adt.internal.project.AndroidManifestParser;
26 import com.android.ide.eclipse.adt.internal.resources.ResourceType;
27 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
28 import com.android.sdklib.SdkConstants;
29 
30 import org.eclipse.core.resources.IContainer;
31 import org.eclipse.core.resources.IFile;
32 import org.eclipse.core.resources.IProject;
33 import org.eclipse.core.resources.IResource;
34 import org.eclipse.core.resources.ResourceAttributes;
35 import org.eclipse.core.resources.ResourcesPlugin;
36 import org.eclipse.core.runtime.CoreException;
37 import org.eclipse.core.runtime.IPath;
38 import org.eclipse.core.runtime.IProgressMonitor;
39 import org.eclipse.core.runtime.OperationCanceledException;
40 import org.eclipse.core.runtime.Path;
41 import org.eclipse.core.runtime.SubMonitor;
42 import org.eclipse.jdt.core.IBuffer;
43 import org.eclipse.jdt.core.ICompilationUnit;
44 import org.eclipse.jdt.core.JavaCore;
45 import org.eclipse.jdt.core.JavaModelException;
46 import org.eclipse.jdt.core.ToolFactory;
47 import org.eclipse.jdt.core.compiler.IScanner;
48 import org.eclipse.jdt.core.compiler.ITerminalSymbols;
49 import org.eclipse.jdt.core.compiler.InvalidInputException;
50 import org.eclipse.jdt.core.dom.AST;
51 import org.eclipse.jdt.core.dom.ASTNode;
52 import org.eclipse.jdt.core.dom.ASTParser;
53 import org.eclipse.jdt.core.dom.CompilationUnit;
54 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
55 import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
56 import org.eclipse.jface.text.ITextSelection;
57 import org.eclipse.ltk.core.refactoring.Change;
58 import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
59 import org.eclipse.ltk.core.refactoring.CompositeChange;
60 import org.eclipse.ltk.core.refactoring.Refactoring;
61 import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
62 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
63 import org.eclipse.ltk.core.refactoring.TextEditChangeGroup;
64 import org.eclipse.ltk.core.refactoring.TextFileChange;
65 import org.eclipse.text.edits.InsertEdit;
66 import org.eclipse.text.edits.MultiTextEdit;
67 import org.eclipse.text.edits.ReplaceEdit;
68 import org.eclipse.text.edits.TextEdit;
69 import org.eclipse.text.edits.TextEditGroup;
70 import org.eclipse.ui.IEditorPart;
71 import org.eclipse.wst.sse.core.StructuredModelManager;
72 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
73 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
74 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
75 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
76 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
77 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
78 import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
79 import org.w3c.dom.Node;
80 
81 import java.io.BufferedReader;
82 import java.io.IOException;
83 import java.io.InputStream;
84 import java.io.InputStreamReader;
85 import java.util.ArrayList;
86 import java.util.HashMap;
87 import java.util.HashSet;
88 import java.util.List;
89 import java.util.Map;
90 
91 /**
92  * This refactoring extracts a string from a file and replaces it by an Android resource ID
93  * such as R.string.foo.
94  * <p/>
95  * There are a number of scenarios, which are not all supported yet. The workflow works as
96  * such:
97  * <ul>
98  * <li> User selects a string in a Java (TODO: or XML file) and invokes
99  *      the {@link ExtractStringAction}.
100  * <li> The action finds the {@link ICompilationUnit} being edited as well as the current
101  *      {@link ITextSelection}. The action creates a new instance of this refactoring as
102  *      well as an {@link ExtractStringWizard} and runs the operation.
103  * <li> Step 1 of the refactoring is to check the preliminary conditions. Right now we check
104  *      that the java source is not read-only and is in sync. We also try to find a string under
105  *      the selection. If this fails, the refactoring is aborted.
106  * <li> TODO: Find the string in an XML file based on selection.
107  * <li> On success, the wizard is shown, which let the user input the new ID to use.
108  * <li> The wizard sets the user input values into this refactoring instance, e.g. the new string
109  *      ID, the XML file to update, etc. The wizard does use the utility method
110  *      {@link XmlStringFileHelper#valueOfStringId(IProject, String, String)} to check whether
111  *      the new ID is already defined in the target XML file.
112  * <li> Once Preview or Finish is selected in the wizard, the
113  *      {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input
114  *      and compute the actual changes.
115  * <li> When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked.
116  * </ul>
117  *
118  * The list of changes are:
119  * <ul>
120  * <li> If the target XML does not exist, create it with the new string ID.
121  * <li> If the target XML exists, find the <resources> node and add the new string ID right after.
122  *      If the node is <resources/>, it needs to be opened.
123  * <li> Create an AST rewriter to edit the source Java file and replace all occurences by the
124  *      new computed R.string.foo. Also need to rewrite imports to import R as needed.
125  *      If there's already a conflicting R included, we need to insert the FQCN instead.
126  * <li> TODO: Have a pref in the wizard: [x] Change other XML Files
127  * <li> TODO: Have a pref in the wizard: [x] Change other Java Files
128  * </ul>
129  */
130 public class ExtractStringRefactoring extends Refactoring {
131 
132     public enum Mode {
133         /**
134          * the Extract String refactoring is called on an <em>existing</em> source file.
135          * Its purpose is then to get the selected string of the source and propose to
136          * change it by an XML id. The XML id may be a new one or an existing one.
137          */
138         EDIT_SOURCE,
139         /**
140          * The Extract String refactoring is called without any source file.
141          * Its purpose is then to create a new XML string ID or select/modify an existing one.
142          */
143         SELECT_ID,
144         /**
145          * The Extract String refactoring is called without any source file.
146          * Its purpose is then to create a new XML string ID. The ID must not already exist.
147          */
148         SELECT_NEW_ID
149     }
150 
151     /** The {@link Mode} of operation of the refactoring. */
152     private final Mode mMode;
153     /** Non-null when editing an Android Resource XML file: identifies the attribute name
154      * of the value being edited. When null, the source is an Android Java file. */
155     private String mXmlAttributeName;
156     /** The file model being manipulated.
157      * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */
158     private final IFile mFile;
159     /** The editor. Non-null when invoked from {@link ExtractStringAction}. Null otherwise. */
160     private final IEditorPart mEditor;
161     /** The project that contains {@link #mFile} and that contains the target XML file to modify. */
162     private final IProject mProject;
163     /** The start of the selection in {@link #mFile}.
164      * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */
165     private final int mSelectionStart;
166     /** The end of the selection in {@link #mFile}.
167      * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */
168     private final int mSelectionEnd;
169 
170     /** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */
171     private ICompilationUnit mUnit;
172     /** The actual string selected, after UTF characters have been escaped, good for display.
173      * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */
174     private String mTokenString;
175 
176     /** The XML string ID selected by the user in the wizard. */
177     private String mXmlStringId;
178     /** The XML string value. Might be different than the initial selected string. */
179     private String mXmlStringValue;
180     /** The path of the XML file that will define {@link #mXmlStringId}, selected by the user
181      *  in the wizard. */
182     private String mTargetXmlFileWsPath;
183 
184     /** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and
185      *  used by {@link #createChange(IProgressMonitor)}. */
186     private ArrayList<Change> mChanges;
187 
188     private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper();
189 
190     private static final String KEY_MODE = "mode";              //$NON-NLS-1$
191     private static final String KEY_FILE = "file";              //$NON-NLS-1$
192     private static final String KEY_PROJECT = "proj";           //$NON-NLS-1$
193     private static final String KEY_SEL_START = "sel-start";    //$NON-NLS-1$
194     private static final String KEY_SEL_END = "sel-end";        //$NON-NLS-1$
195     private static final String KEY_TOK_ESC = "tok-esc";        //$NON-NLS-1$
196     private static final String KEY_XML_ATTR_NAME = "xml-attr-name";      //$NON-NLS-1$
197 
ExtractStringRefactoring(Map<String, String> arguments)198     public ExtractStringRefactoring(Map<String, String> arguments) throws NullPointerException {
199         mMode = Mode.valueOf(arguments.get(KEY_MODE));
200 
201         IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT));
202         mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
203 
204         if (mMode == Mode.EDIT_SOURCE) {
205             path = Path.fromPortableString(arguments.get(KEY_FILE));
206             mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
207 
208             mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START));
209             mSelectionEnd   = Integer.parseInt(arguments.get(KEY_SEL_END));
210             mTokenString    = arguments.get(KEY_TOK_ESC);
211             mXmlAttributeName = arguments.get(KEY_XML_ATTR_NAME);
212         } else {
213             mFile = null;
214             mSelectionStart = mSelectionEnd = -1;
215             mTokenString = null;
216             mXmlAttributeName = null;
217         }
218 
219         mEditor = null;
220     }
221 
createArgumentMap()222     private Map<String, String> createArgumentMap() {
223         HashMap<String, String> args = new HashMap<String, String>();
224         args.put(KEY_MODE,      mMode.name());
225         args.put(KEY_PROJECT,   mProject.getFullPath().toPortableString());
226         if (mMode == Mode.EDIT_SOURCE) {
227             args.put(KEY_FILE,      mFile.getFullPath().toPortableString());
228             args.put(KEY_SEL_START, Integer.toString(mSelectionStart));
229             args.put(KEY_SEL_END,   Integer.toString(mSelectionEnd));
230             args.put(KEY_TOK_ESC,   mTokenString);
231             args.put(KEY_XML_ATTR_NAME, mXmlAttributeName);
232         }
233         return args;
234     }
235 
236     /**
237      * Constructor to use when the Extract String refactoring is called on an
238      * *existing* source file. Its purpose is then to get the selected string of
239      * the source and propose to change it by an XML id. The XML id may be a new one
240      * or an existing one.
241      *
242      * @param file The source file to process. Cannot be null. File must exist in workspace.
243      * @param editor
244      * @param selection The selection in the source file. Cannot be null or empty.
245      */
ExtractStringRefactoring(IFile file, IEditorPart editor, ITextSelection selection)246     public ExtractStringRefactoring(IFile file, IEditorPart editor, ITextSelection selection) {
247         mMode = Mode.EDIT_SOURCE;
248         mFile = file;
249         mEditor = editor;
250         mProject = file.getProject();
251         mSelectionStart = selection.getOffset();
252         mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1);
253     }
254 
255     /**
256      * Constructor to use when the Extract String refactoring is called without
257      * any source file. Its purpose is then to create a new XML string ID.
258      *
259      * @param project The project where the target XML file to modify is located. Cannot be null.
260      * @param enforceNew If true the XML ID must be a new one. If false, an existing ID can be
261      *  used.
262      */
ExtractStringRefactoring(IProject project, boolean enforceNew)263     public ExtractStringRefactoring(IProject project, boolean enforceNew) {
264         mMode = enforceNew ? Mode.SELECT_NEW_ID : Mode.SELECT_ID;
265         mFile = null;
266         mEditor = null;
267         mProject = project;
268         mSelectionStart = mSelectionEnd = -1;
269     }
270 
271     /**
272      * @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
273      */
274     @Override
getName()275     public String getName() {
276         if (mMode == Mode.SELECT_ID) {
277             return "Create or USe Android String";
278         } else if (mMode == Mode.SELECT_NEW_ID) {
279             return "Create New Android String";
280         }
281 
282         return "Extract Android String";
283     }
284 
getMode()285     public Mode getMode() {
286         return mMode;
287     }
288 
289     /**
290      * Gets the actual string selected, after UTF characters have been escaped,
291      * good for display.
292      */
getTokenString()293     public String getTokenString() {
294         return mTokenString;
295     }
296 
getXmlStringId()297     public String getXmlStringId() {
298         return mXmlStringId;
299     }
300 
301     /**
302      * Step 1 of 3 of the refactoring:
303      * Checks that the current selection meets the initial condition before the ExtractString
304      * wizard is shown. The check is supposed to be lightweight and quick. Note that at that
305      * point the wizard has not been created yet.
306      * <p/>
307      * Here we scan the source buffer to find the token matching the selection.
308      * The check is successful is a Java string literal is selected, the source is in sync
309      * and is not read-only.
310      * <p/>
311      * This is also used to extract the string to be modified, so that we can display it in
312      * the refactoring wizard.
313      *
314      * @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor)
315      *
316      * @throws CoreException
317      */
318     @Override
checkInitialConditions(IProgressMonitor monitor)319     public RefactoringStatus checkInitialConditions(IProgressMonitor monitor)
320             throws CoreException, OperationCanceledException {
321 
322         mUnit = null;
323         mTokenString = null;
324 
325         RefactoringStatus status = new RefactoringStatus();
326 
327         try {
328             monitor.beginTask("Checking preconditions...", 6);
329 
330             if (mMode != Mode.EDIT_SOURCE) {
331                 monitor.worked(6);
332                 return status;
333             }
334 
335             if (!checkSourceFile(mFile, status, monitor)) {
336                 return status;
337             }
338 
339             // Try to get a compilation unit from this file. If it fails, mUnit is null.
340             try {
341                 mUnit = JavaCore.createCompilationUnitFrom(mFile);
342 
343                 // Make sure the unit is not read-only, e.g. it's not a class file or inside a Jar
344                 if (mUnit.isReadOnly()) {
345                     status.addFatalError("The file is read-only, please make it writeable first.");
346                     return status;
347                 }
348 
349                 // This is a Java file. Check if it contains the selection we want.
350                 if (!findSelectionInJavaUnit(mUnit, status, monitor)) {
351                     return status;
352                 }
353 
354             } catch (Exception e) {
355                 // That was not a Java file. Ignore.
356             }
357 
358             if (mUnit != null) {
359                 monitor.worked(1);
360                 return status;
361             }
362 
363             // Check this a Layout XML file and get the selection and its context.
364             if (mFile != null && AndroidConstants.EXT_XML.equals(mFile.getFileExtension())) {
365 
366                 // Currently we only support Android resource XML files, so they must have a path
367                 // similar to
368                 //    project/res/<type>[-<configuration>]/*.xml
369                 // There is no support for sub folders, so the segment count must be 4.
370                 // We don't need to check the type folder name because a/ we only accept
371                 // an AndroidEditor source and b/ aapt generates a compilation error for
372                 // unknown folders.
373                 IPath path = mFile.getFullPath();
374                 // check if we are inside the project/res/* folder.
375                 if (path.segmentCount() == 4) {
376                     if (path.segment(1).equalsIgnoreCase(SdkConstants.FD_RESOURCES)) {
377                         if (!findSelectionInXmlFile(mFile, status, monitor)) {
378                             return status;
379                         }
380                     }
381                 }
382             }
383 
384             if (!status.isOK()) {
385                 status.addFatalError(
386                         "Selection must be inside a Java source or an Android Layout XML file.");
387             }
388 
389         } finally {
390             monitor.done();
391         }
392 
393         return status;
394     }
395 
396     /**
397      * Try to find the selected Java element in the compilation unit.
398      *
399      * If selection matches a string literal, capture it, otherwise add a fatal error
400      * to the status.
401      *
402      * On success, advance the monitor by 3.
403      * Returns status.isOK().
404      */
findSelectionInJavaUnit(ICompilationUnit unit, RefactoringStatus status, IProgressMonitor monitor)405     private boolean findSelectionInJavaUnit(ICompilationUnit unit,
406             RefactoringStatus status, IProgressMonitor monitor) {
407         try {
408             IBuffer buffer = unit.getBuffer();
409 
410             IScanner scanner = ToolFactory.createScanner(
411                     false, //tokenizeComments
412                     false, //tokenizeWhiteSpace
413                     false, //assertMode
414                     false  //recordLineSeparator
415                     );
416             scanner.setSource(buffer.getCharacters());
417             monitor.worked(1);
418 
419             for(int token = scanner.getNextToken();
420                     token != ITerminalSymbols.TokenNameEOF;
421                     token = scanner.getNextToken()) {
422                 if (scanner.getCurrentTokenStartPosition() <= mSelectionStart &&
423                         scanner.getCurrentTokenEndPosition() >= mSelectionEnd) {
424                     // found the token, but only keep if the right type
425                     if (token == ITerminalSymbols.TokenNameStringLiteral) {
426                         mTokenString = new String(scanner.getCurrentTokenSource());
427                     }
428                     break;
429                 } else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) {
430                     // scanner is past the selection, abort.
431                     break;
432                 }
433             }
434         } catch (JavaModelException e1) {
435             // Error in unit.getBuffer. Ignore.
436         } catch (InvalidInputException e2) {
437             // Error in scanner.getNextToken. Ignore.
438         } finally {
439             monitor.worked(1);
440         }
441 
442         if (mTokenString != null) {
443             // As a literal string, the token should have surrounding quotes. Remove them.
444             int len = mTokenString.length();
445             if (len > 0 &&
446                     mTokenString.charAt(0) == '"' &&
447                     mTokenString.charAt(len - 1) == '"') {
448                 mTokenString = mTokenString.substring(1, len - 1);
449             }
450             // We need a non-empty string literal
451             if (mTokenString.length() == 0) {
452                 mTokenString = null;
453             }
454         }
455 
456         if (mTokenString == null) {
457             status.addFatalError("Please select a Java string literal.");
458         }
459 
460         monitor.worked(1);
461         return status.isOK();
462     }
463     /**
464      * Try to find the selected XML element. This implementation replies on the refactoring
465      * originating from an Android Layout Editor. We rely on some internal properties of the
466      * Structured XML editor to retrieve file content to avoid parsing it again. We also rely
467      * on our specific Android XML model to get element & attribute descriptor properties.
468      *
469      * If selection matches a string literal, capture it, otherwise add a fatal error
470      * to the status.
471      *
472      * On success, advance the monitor by 1.
473      * Returns status.isOK().
474      */
findSelectionInXmlFile(IFile file, RefactoringStatus status, IProgressMonitor monitor)475     private boolean findSelectionInXmlFile(IFile file,
476             RefactoringStatus status,
477             IProgressMonitor monitor) {
478 
479         try {
480             if (!(mEditor instanceof AndroidEditor)) {
481                 status.addFatalError("Only the Android XML Editor is currently supported.");
482                 return status.isOK();
483             }
484 
485             AndroidEditor editor = (AndroidEditor) mEditor;
486             IStructuredModel smodel = null;
487             Node node = null;
488             String currAttrName = null;
489 
490             try {
491                 // See the portability note in AndroidEditor#getModelForRead() javadoc.
492                 smodel = editor.getModelForRead();
493                 if (smodel != null) {
494                     // The structured model gives the us the actual XML Node element where the
495                     // offset is. By using this Node, we can find the exact UiElementNode of our
496                     // model and thus we'll be able to get the properties of the attribute -- to
497                     // check if it accepts a string reference. This does not however tell us if
498                     // the selection is actually in an attribute value, nor which attribute is
499                     // being edited.
500                     for(int offset = mSelectionStart; offset >= 0 && node == null; --offset) {
501                         node = (Node) smodel.getIndexedRegion(offset);
502                     }
503 
504                     if (node == null) {
505                         status.addFatalError(
506                                 "The selection does not match any element in the XML document.");
507                         return status.isOK();
508                     }
509 
510                     if (node.getNodeType() != Node.ELEMENT_NODE) {
511                         status.addFatalError("The selection is not inside an actual XML element.");
512                         return status.isOK();
513                     }
514 
515                     IStructuredDocument sdoc = smodel.getStructuredDocument();
516                     if (sdoc != null) {
517                         // Portability note: all the structured document implementation is
518                         // under wst.sse.core.internal.provisional so we can expect it to change in
519                         // a distant future if they start cleaning their codebase, however unlikely
520                         // that is.
521 
522                         int selStart = mSelectionStart;
523                         IStructuredDocumentRegion region =
524                             sdoc.getRegionAtCharacterOffset(selStart);
525                         if (region != null &&
526                                 DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
527                             // Find if any sub-region representing an attribute contains the
528                             // selection. If it does, returns the name of the attribute in
529                             // currAttrName and returns the value in the field mTokenString.
530                             currAttrName = findSelectionInRegion(region, selStart);
531 
532                             if (mTokenString == null) {
533                                 status.addFatalError(
534                                     "The selection is not inside an actual XML attribute value.");
535                             }
536                         }
537                     }
538 
539                     if (mTokenString != null && node != null && currAttrName != null) {
540 
541                         // Validate that the attribute accepts a string reference.
542                         // This sets mTokenString to null by side-effect when it fails and
543                         // adds a fatal error to the status as needed.
544                         validateSelectedAttribute(editor, node, currAttrName, status);
545 
546                     } else {
547                         // We shouldn't get here: we're missing one of the token string, the node
548                         // or the attribute name. All of them have been checked earlier so don't
549                         // set any specific error.
550                         mTokenString = null;
551                     }
552                 }
553             } finally {
554                 if (smodel != null) {
555                     smodel.releaseFromRead();
556                 }
557             }
558 
559         } finally {
560             monitor.worked(1);
561         }
562 
563         return status.isOK();
564     }
565 
566     /**
567      * The region gives us the textual representation of the XML element
568      * where the selection starts, split using sub-regions. We now just
569      * need to iterate through the sub-regions to find which one
570      * contains the actual selection. We're interested in an attribute
571      * value however when we find one we want to memorize the attribute
572      * name that was defined just before.
573      *
574      * @return When the cursor is on a valid attribute name or value, returns the string of
575      * attribute name. As a side-effect, returns the value of the attribute in {@link #mTokenString}
576      */
findSelectionInRegion(IStructuredDocumentRegion region, int selStart)577     private String findSelectionInRegion(IStructuredDocumentRegion region, int selStart) {
578 
579         String currAttrName = null;
580 
581         int startInRegion = selStart - region.getStartOffset();
582 
583         int nb = region.getNumberOfRegions();
584         ITextRegionList list = region.getRegions();
585         String currAttrValue = null;
586 
587         for (int i = 0; i < nb; i++) {
588             ITextRegion subRegion = list.get(i);
589             String type = subRegion.getType();
590 
591             if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
592                 currAttrName = region.getText(subRegion);
593 
594                 // I like to select the attribute definition and invoke
595                 // the extract string wizard. So if the selection is on
596                 // the attribute name part, find the value that is just
597                 // after and use it as if it were the selection.
598 
599                 if (subRegion.getStart() <= startInRegion &&
600                         startInRegion < subRegion.getTextEnd()) {
601                     // A well-formed attribute is composed of a name,
602                     // an equal sign and the value. There can't be any space
603                     // in between, which makes the parsing a lot easier.
604                     if (i <= nb - 3 &&
605                             DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals(
606                                                    list.get(i + 1).getType())) {
607                         subRegion = list.get(i + 2);
608                         type = subRegion.getType();
609                         if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(
610                                 type)) {
611                             currAttrValue = region.getText(subRegion);
612                         }
613                     }
614                 }
615 
616             } else if (subRegion.getStart() <= startInRegion &&
617                     startInRegion < subRegion.getTextEnd() &&
618                     DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
619                 currAttrValue = region.getText(subRegion);
620             }
621 
622             if (currAttrValue != null) {
623                 // We found the value. Only accept it if not empty
624                 // and if we found an attribute name before.
625                 String text = currAttrValue;
626 
627                 // The attribute value will contain the XML quotes. Remove them.
628                 int len = text.length();
629                 if (len >= 2 &&
630                         text.charAt(0) == '"' &&
631                         text.charAt(len - 1) == '"') {
632                     text = text.substring(1, len - 1);
633                 } else if (len >= 2 &&
634                         text.charAt(0) == '\'' &&
635                         text.charAt(len - 1) == '\'') {
636                     text = text.substring(1, len - 1);
637                 }
638                 if (text.length() > 0 && currAttrName != null) {
639                     // Setting mTokenString to non-null marks the fact we
640                     // accept this attribute.
641                     mTokenString = text;
642                 }
643 
644                 break;
645             }
646         }
647 
648         return currAttrName;
649     }
650 
651     /**
652      * Validates that the attribute accepts a string reference.
653      * This sets mTokenString to null by side-effect when it fails and
654      * adds a fatal error to the status as needed.
655      */
validateSelectedAttribute(AndroidEditor editor, Node node, String attrName, RefactoringStatus status)656     private void validateSelectedAttribute(AndroidEditor editor, Node node,
657             String attrName, RefactoringStatus status) {
658         UiElementNode rootUiNode = editor.getUiRootNode();
659         UiElementNode currentUiNode =
660             rootUiNode == null ? null : rootUiNode.findXmlNode(node);
661         ReferenceAttributeDescriptor attrDesc = null;
662 
663         if (currentUiNode != null) {
664             // remove any namespace prefix from the attribute name
665             String name = attrName;
666             int pos = name.indexOf(':');
667             if (pos > 0 && pos < name.length() - 1) {
668                 name = name.substring(pos + 1);
669             }
670 
671             for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) {
672                 if (attrNode.getDescriptor().getXmlLocalName().equals(name)) {
673                     AttributeDescriptor desc = attrNode.getDescriptor();
674                     if (desc instanceof ReferenceAttributeDescriptor) {
675                         attrDesc = (ReferenceAttributeDescriptor) desc;
676                     }
677                     break;
678                 }
679             }
680         }
681 
682         // The attribute descriptor is a resource reference. It must either accept
683         // of any resource type or specifically accept string types.
684         if (attrDesc != null &&
685                 (attrDesc.getResourceType() == null ||
686                  attrDesc.getResourceType() == ResourceType.STRING)) {
687             // We have one more check to do: is the current string value already
688             // an Android XML string reference? If so, we can't edit it.
689             if (mTokenString.startsWith("@")) {                             //$NON-NLS-1$
690                 int pos1 = 0;
691                 if (mTokenString.length() > 1 && mTokenString.charAt(1) == '+') {
692                     pos1++;
693                 }
694                 int pos2 = mTokenString.indexOf('/');
695                 if (pos2 > pos1) {
696                     String kind = mTokenString.substring(pos1 + 1, pos2);
697                     if (ResourceType.STRING.getName().equals(kind)) {                            //$NON-NLS-1$
698                         mTokenString = null;
699                         status.addFatalError(String.format(
700                                 "The attribute %1$s already contains a %2$s reference.",
701                                 attrName,
702                                 kind));
703                     }
704                 }
705             }
706 
707             if (mTokenString != null) {
708                 // We're done with all our checks. mTokenString contains the
709                 // current attribute value. We don't memorize the region nor the
710                 // attribute, however we memorize the textual attribute name so
711                 // that we can offer replacement for all its occurrences.
712                 mXmlAttributeName = attrName;
713             }
714 
715         } else {
716             mTokenString = null;
717             status.addFatalError(String.format(
718                     "The attribute %1$s does not accept a string reference.",
719                     attrName));
720         }
721     }
722 
723     /**
724      * Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit()
725      * Might not be useful.
726      *
727      * On success, advance the monitor by 2.
728      *
729      * @return False if caller should abort, true if caller should continue.
730      */
checkSourceFile(IFile file, RefactoringStatus status, IProgressMonitor monitor)731     private boolean checkSourceFile(IFile file,
732             RefactoringStatus status,
733             IProgressMonitor monitor) {
734         // check whether the source file is in sync
735         if (!file.isSynchronized(IResource.DEPTH_ZERO)) {
736             status.addFatalError("The file is not synchronized. Please save it first.");
737             return false;
738         }
739         monitor.worked(1);
740 
741         // make sure we can write to it.
742         ResourceAttributes resAttr = file.getResourceAttributes();
743         if (resAttr == null || resAttr.isReadOnly()) {
744             status.addFatalError("The file is read-only, please make it writeable first.");
745             return false;
746         }
747         monitor.worked(1);
748 
749         return true;
750     }
751 
752     /**
753      * Step 2 of 3 of the refactoring:
754      * Check the conditions once the user filled values in the refactoring wizard,
755      * then prepare the changes to be applied.
756      * <p/>
757      * In this case, most of the sanity checks are done by the wizard so essentially this
758      * should only be called if the wizard positively validated the user input.
759      *
760      * Here we do check that the target resource XML file either does not exists or
761      * is not read-only.
762      *
763      * @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor)
764      *
765      * @throws CoreException
766      */
767     @Override
checkFinalConditions(IProgressMonitor monitor)768     public RefactoringStatus checkFinalConditions(IProgressMonitor monitor)
769             throws CoreException, OperationCanceledException {
770         RefactoringStatus status = new RefactoringStatus();
771 
772         try {
773             monitor.beginTask("Checking post-conditions...", 3);
774 
775             if (mXmlStringId == null || mXmlStringId.length() <= 0) {
776                 // this is not supposed to happen
777                 status.addFatalError("Missing replacement string ID");
778             } else if (mTargetXmlFileWsPath == null || mTargetXmlFileWsPath.length() <= 0) {
779                 // this is not supposed to happen
780                 status.addFatalError("Missing target xml file path");
781             }
782             monitor.worked(1);
783 
784             // Either that resource must not exist or it must be a writeable file.
785             IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath);
786             if (targetXml != null) {
787                 if (targetXml.getType() != IResource.FILE) {
788                     status.addFatalError(
789                             String.format("XML file '%1$s' is not a file.", mTargetXmlFileWsPath));
790                 } else {
791                     ResourceAttributes attr = targetXml.getResourceAttributes();
792                     if (attr != null && attr.isReadOnly()) {
793                         status.addFatalError(
794                                 String.format("XML file '%1$s' is read-only.",
795                                         mTargetXmlFileWsPath));
796                     }
797                 }
798             }
799             monitor.worked(1);
800 
801             if (status.hasError()) {
802                 return status;
803             }
804 
805             mChanges = new ArrayList<Change>();
806 
807 
808             // Prepare the change for the XML file.
809 
810             if (mXmlHelper.valueOfStringId(mProject, mTargetXmlFileWsPath, mXmlStringId) == null) {
811                 // We actually change it only if the ID doesn't exist yet
812                 Change change = createXmlChange((IFile) targetXml, mXmlStringId, mXmlStringValue,
813                         status, SubMonitor.convert(monitor, 1));
814                 if (change != null) {
815                     mChanges.add(change);
816                 }
817             }
818 
819             if (status.hasError()) {
820                 return status;
821             }
822 
823             if (mMode == Mode.EDIT_SOURCE) {
824                 List<Change> changes = null;
825                 if (mXmlAttributeName != null) {
826                     // Prepare the change to the Android resource XML file
827                     changes = computeXmlSourceChanges(mFile,
828                             mXmlStringId, mTokenString, mXmlAttributeName,
829                             status, monitor);
830 
831                 } else {
832                     // Prepare the change to the Java compilation unit
833                     changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
834                             status, SubMonitor.convert(monitor, 1));
835                 }
836                 if (changes != null) {
837                     mChanges.addAll(changes);
838                 }
839             }
840 
841             monitor.worked(1);
842         } finally {
843             monitor.done();
844         }
845 
846         return status;
847     }
848 
849     /**
850      * Internal helper that actually prepares the {@link Change} that adds the given
851      * ID to the given XML File.
852      * <p/>
853      * This does not actually modify the file.
854      *
855      * @param targetXml The file resource to modify.
856      * @param xmlStringId The new ID to insert.
857      * @param tokenString The old string, which will be the value in the XML string.
858      * @return A new {@link TextEdit} that describes how to change the file.
859      */
createXmlChange(IFile targetXml, String xmlStringId, String tokenString, RefactoringStatus status, SubMonitor subMonitor)860     private Change createXmlChange(IFile targetXml,
861             String xmlStringId,
862             String tokenString,
863             RefactoringStatus status,
864             SubMonitor subMonitor) {
865 
866         TextFileChange xmlChange = new TextFileChange(getName(), targetXml);
867         xmlChange.setTextType("xml");   //$NON-NLS-1$
868 
869         TextEdit edit = null;
870         TextEditGroup editGroup = null;
871 
872         if (!targetXml.exists()) {
873             // The XML file does not exist. Simply create it.
874             StringBuilder content = new StringBuilder();
875             content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
876             content.append("<resources>\n");                                //$NON-NLS-1$
877             content.append("    <string name=\"").                          //$NON-NLS-1$
878                         append(xmlStringId).
879                         append("\">").                                      //$NON-NLS-1$
880                         append(tokenString).
881                         append("</string>\n");                              //$NON-NLS-1$
882             content.append("</resources>\n");                                //$NON-NLS-1$
883 
884             edit = new InsertEdit(0, content.toString());
885             editGroup = new TextEditGroup("Create <string> in new XML file", edit);
886         } else {
887             // The file exist. Attempt to parse it as a valid XML document.
888             try {
889                 int[] indices = new int[2];
890 
891                 // TODO case where we replace the value of an existing XML String ID
892 
893                 if (findXmlOpeningTagPos(targetXml.getContents(), "resources", indices)) {  //$NON-NLS-1$
894                     // Indices[1] indicates whether we found > or />. It can only be 1 or 2.
895                     // Indices[0] is the position of the first character of either > or />.
896                     //
897                     // Note: we don't even try to adapt our formatting to the existing structure (we
898                     // could by capturing whatever whitespace is after the closing bracket and
899                     // applying it here before our tag, unless we were dealing with an empty
900                     // resource tag.)
901 
902                     int offset = indices[0];
903                     int len = indices[1];
904                     StringBuilder content = new StringBuilder();
905                     content.append(">\n");                                      //$NON-NLS-1$
906                     content.append("    <string name=\"").                      //$NON-NLS-1$
907                                 append(xmlStringId).
908                                 append("\">").                                  //$NON-NLS-1$
909                                 append(tokenString).
910                                 append("</string>");                            //$NON-NLS-1$
911                     if (len == 2) {
912                         content.append("\n</resources>");                       //$NON-NLS-1$
913                     }
914 
915                     edit = new ReplaceEdit(offset, len, content.toString());
916                     editGroup = new TextEditGroup("Insert <string> in XML file", edit);
917                 }
918             } catch (CoreException e) {
919                 // Failed to read file. Ignore. Will return null below.
920             }
921         }
922 
923         if (edit == null) {
924             status.addFatalError(String.format("Failed to modify file %1$s",
925                     mTargetXmlFileWsPath));
926             return null;
927         }
928 
929         xmlChange.setEdit(edit);
930         // The TextEditChangeGroup let the user toggle this change on and off later.
931         xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup));
932 
933         subMonitor.worked(1);
934         return xmlChange;
935     }
936 
937     /**
938      * Parse an XML input stream, looking for an opening tag.
939      * <p/>
940      * If found, returns the character offest in the buffer of the closing bracket of that
941      * tag, e.g. the position of > in "<resources>". The first character is at offset 0.
942      * <p/>
943      * The implementation here relies on a simple character-based parser. No DOM nor SAX
944      * parsing is used, due to the simplified nature of the task: we just want the first
945      * opening tag, which in our case should be the document root. We deal however with
946      * with the tag being commented out, so comments are skipped. We assume the XML doc
947      * is sane, e.g. we don't expect the tag to appear in the middle of a string. But
948      * again since in fact we want the root element, that's unlikely to happen.
949      * <p/>
950      * We need to deal with the case where the element is written as <resources/>, in
951      * which case the caller will want to replace /> by ">...</...>". To do that we return
952      * two values: the first offset of the closing tag (e.g. / or >) and the length, which
953      * can only be 1 or 2. If it's 2, the caller has to deal with /> instead of just >.
954      *
955      * @param contents An existing buffer to parse.
956      * @param tag The tag to look for.
957      * @param indices The return values: [0] is the offset of the closing bracket and [1] is
958      *          the length which can be only 1 for > and 2 for />
959      * @return True if we found the tag, in which case <code>indices</code> can be used.
960      */
findXmlOpeningTagPos(InputStream contents, String tag, int[] indices)961     private boolean findXmlOpeningTagPos(InputStream contents, String tag, int[] indices) {
962 
963         BufferedReader br = new BufferedReader(new InputStreamReader(contents));
964         StringBuilder sb = new StringBuilder(); // scratch area
965 
966         tag = "<" + tag;
967         int tagLen = tag.length();
968         int maxLen = tagLen < 3 ? 3 : tagLen;
969 
970         try {
971             int offset = 0;
972             int i = 0;
973             char searching = '<'; // we want opening tags
974             boolean capture = false;
975             boolean inComment = false;
976             boolean inTag = false;
977             while ((i = br.read()) != -1) {
978                 char c = (char) i;
979                 if (c == searching) {
980                     capture = true;
981                 }
982                 if (capture) {
983                     sb.append(c);
984                     int len = sb.length();
985                     if (inComment && c == '>') {
986                         // is the comment being closed?
987                         if (len >= 3 && sb.substring(len-3).equals("-->")) {    //$NON-NLS-1$
988                             // yes, comment is closing, stop capturing
989                             capture = false;
990                             inComment = false;
991                             sb.setLength(0);
992                         }
993                     } else if (inTag && c == '>') {
994                         // we're capturing in our tag, waiting for the closing >, we just got it
995                         // so we're totally done here. Simply detect whether it's /> or >.
996                         indices[0] = offset;
997                         indices[1] = 1;
998                         if (sb.charAt(len - 2) == '/') {
999                             indices[0]--;
1000                             indices[1]++;
1001                         }
1002                         return true;
1003 
1004                     } else if (!inComment && !inTag) {
1005                         // not a comment and not our tag yet, so we're capturing because a
1006                         // tag is being opened but we don't know which one yet.
1007 
1008                         // look for either the opening or a comment or
1009                         // the opening of our tag.
1010                         if (len == 3 && sb.equals("<--")) {                     //$NON-NLS-1$
1011                             inComment = true;
1012                         } else if (len == tagLen && sb.toString().equals(tag)) {
1013                             inTag = true;
1014                         }
1015 
1016                         // if we're not interested in this tag yet, deal with when to stop
1017                         // capturing: the opening tag ends with either any kind of whitespace
1018                         // or with a > or maybe there's a PI that starts with <?
1019                         if (!inComment && !inTag) {
1020                             if (c == '>' || c == '?' || c == ' ' || c == '\n' || c == '\r') {
1021                                 // stop capturing
1022                                 capture = false;
1023                                 sb.setLength(0);
1024                             }
1025                         }
1026                     }
1027 
1028                     if (capture && len > maxLen) {
1029                         // in any case we don't need to capture more than the size of our tag
1030                         // or the comment opening tag
1031                         sb.deleteCharAt(0);
1032                     }
1033                 }
1034                 offset++;
1035             }
1036         } catch (IOException e) {
1037             // Ignore.
1038         } finally {
1039             try {
1040                 br.close();
1041             } catch (IOException e) {
1042                 // oh come on...
1043             }
1044         }
1045 
1046         return false;
1047     }
1048 
1049 
1050     /**
1051      * Computes the changes to be made to the source Android XML file(s) and
1052      * returns a list of {@link Change}.
1053      */
1054     private List<Change> computeXmlSourceChanges(IFile sourceFile,
1055             String xmlStringId,
1056             String tokenString,
1057             String xmlAttrName,
1058             RefactoringStatus status,
1059             IProgressMonitor monitor) {
1060 
1061         if (!sourceFile.exists()) {
1062             status.addFatalError(String.format("XML file '%1$s' does not exist.",
1063                     sourceFile.getFullPath().toOSString()));
1064             return null;
1065         }
1066 
1067         // In the initial condition check we validated that this file is part of
1068         // an Android resource folder, with a folder path that looks like
1069         //   /project/res/<type>-<configuration>/<filename.xml>
1070         // Here we are going to offer XML source change for the same filename accross all
1071         // configurations of the same res type. E.g. if we're processing a res/layout/main.xml
1072         // file then we want to offer changes for res/layout-fr/main.xml. We compute such a
1073         // list here.
1074         HashSet<IFile> files = new HashSet<IFile>();
1075         files.add(sourceFile);
1076 
1077         if (AndroidConstants.EXT_XML.equals(sourceFile.getFileExtension())) {
1078             IPath path = sourceFile.getFullPath();
1079             if (path.segmentCount() == 4 && path.segment(1).equals(SdkConstants.FD_RESOURCES)) {
1080                 IProject project = sourceFile.getProject();
1081                 String filename = path.segment(3);
1082                 String initialTypeName = path.segment(2);
1083                 ResourceFolderType type = ResourceFolderType.getFolderType(initialTypeName);
1084 
1085                 IContainer res = sourceFile.getParent().getParent();
1086                 if (type != null && res != null && res.getType() == IResource.FOLDER) {
1087                     try {
1088                         for (IResource r : res.members()) {
1089                             if (r != null && r.getType() == IResource.FOLDER) {
1090                                 String name = r.getName();
1091                                 // Skip the initial folder name, it's already in the list.
1092                                 if (!name.equals(initialTypeName)) {
1093                                     // Only accept the same folder type (e.g. layout-*)
1094                                     ResourceFolderType t =
1095                                         ResourceFolderType.getFolderType(name);
1096                                     if (type.equals(t)) {
1097                                         // recompute the path
1098                                         IPath p = res.getProjectRelativePath().append(name).
1099                                                                                append(filename);
1100                                         IResource f = project.findMember(p);
1101                                         if (f != null && f instanceof IFile) {
1102                                             files.add((IFile) f);
1103                                         }
1104                                     }
1105                                 }
1106                             }
1107                         }
1108                     } catch (CoreException e) {
1109                         // Ignore.
1110                     }
1111                 }
1112             }
1113         }
1114 
1115         SubMonitor subMonitor = SubMonitor.convert(monitor, Math.min(1, files.size()));
1116 
1117         ArrayList<Change> changes = new ArrayList<Change>();
1118 
1119         try {
1120             // Portability note: getModelManager is part of wst.sse.core however the
1121             // interface returned is part of wst.sse.core.internal.provisional so we can
1122             // expect it to change in a distant future if they start cleaning their codebase,
1123             // however unlikely that is.
1124             IModelManager modelManager = StructuredModelManager.getModelManager();
1125 
1126             for (IFile file : files) {
1127 
1128                 IStructuredDocument sdoc = modelManager.createStructuredDocumentFor(file);
1129 
1130                 if (sdoc == null) {
1131                     status.addFatalError("XML structured document not found");     //$NON-NLS-1$
1132                     return null;
1133                 }
1134 
1135                 TextFileChange xmlChange = new TextFileChange(getName(), file);
1136                 xmlChange.setTextType("xml");   //$NON-NLS-1$
1137 
1138                 MultiTextEdit multiEdit = new MultiTextEdit();
1139                 ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>();
1140 
1141                 String quotedReplacement = quotedAttrValue("@string/" + xmlStringId);
1142 
1143                 // Prepare the change set
1144                 try {
1145                     for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) {
1146                         // Only look at XML "top regions"
1147                         if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
1148                             continue;
1149                         }
1150 
1151                         int nb = region.getNumberOfRegions();
1152                         ITextRegionList list = region.getRegions();
1153                         String lastAttrName = null;
1154 
1155                         for (int i = 0; i < nb; i++) {
1156                             ITextRegion subRegion = list.get(i);
1157                             String type = subRegion.getType();
1158 
1159                             if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
1160                                 // Memorize the last attribute name seen
1161                                 lastAttrName = region.getText(subRegion);
1162 
1163                             } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
1164                                 // Check this is the attribute and the original string
1165                                 String text = region.getText(subRegion);
1166 
1167                                 int len = text.length();
1168                                 if (len >= 2 &&
1169                                         text.charAt(0) == '"' &&
1170                                         text.charAt(len - 1) == '"') {
1171                                     text = text.substring(1, len - 1);
1172                                 } else if (len >= 2 &&
1173                                         text.charAt(0) == '\'' &&
1174                                         text.charAt(len - 1) == '\'') {
1175                                     text = text.substring(1, len - 1);
1176                                 }
1177 
1178                                 if (xmlAttrName.equals(lastAttrName) && tokenString.equals(text)) {
1179 
1180                                     // Found an occurrence. Create a change for it.
1181                                     TextEdit edit = new ReplaceEdit(
1182                                             region.getStartOffset() + subRegion.getStart(),
1183                                             subRegion.getTextLength(),
1184                                             quotedReplacement);
1185                                     TextEditGroup editGroup = new TextEditGroup(
1186                                             "Replace attribute string by ID",
1187                                             edit);
1188 
1189                                     multiEdit.addChild(edit);
1190                                     editGroups.add(editGroup);
1191                                 }
1192                             }
1193                         }
1194                     }
1195                 } catch (Throwable t) {
1196                     // Since we use some internal APIs, use a broad catch-all to report any
1197                     // unexpected issue rather than crash the whole refactoring.
1198                     status.addFatalError(
1199                             String.format("XML refactoring error: %1$s", t.getMessage()));
1200                 } finally {
1201                     if (multiEdit.hasChildren()) {
1202                         xmlChange.setEdit(multiEdit);
1203                         for (TextEditGroup group : editGroups) {
1204                             xmlChange.addTextEditChangeGroup(
1205                                     new TextEditChangeGroup(xmlChange, group));
1206                         }
1207                         changes.add(xmlChange);
1208                     }
1209                     subMonitor.worked(1);
1210                 }
1211             } // for files
1212 
1213         } catch (IOException e) {
1214             status.addFatalError(String.format("XML model IO error: %1$s.", e.getMessage()));
1215         } catch (CoreException e) {
1216             status.addFatalError(String.format("XML model core error: %1$s.", e.getMessage()));
1217         } finally {
1218             if (changes.size() > 0) {
1219                 return changes;
1220             }
1221         }
1222 
1223         return null;
1224     }
1225 
1226     /**
1227      * Returns a quoted attribute value suitable to be placed after an attributeName=
1228      * statement in an XML stream.
1229      *
1230      * According to http://www.w3.org/TR/2008/REC-xml-20081126/#NT-AttValue
1231      * the attribute value can be either quoted using ' or " and the corresponding
1232      * entities &apos; or &quot; must be used inside.
1233      */
1234     private String quotedAttrValue(String attrValue) {
1235         if (attrValue.indexOf('"') == -1) {
1236             // no double-quotes inside, use double-quotes around.
1237             return '"' + attrValue + '"';
1238         }
1239         if (attrValue.indexOf('\'') == -1) {
1240             // no single-quotes inside, use single-quotes around.
1241             return '\'' + attrValue + '\'';
1242         }
1243         // If we get here, there's a mix. Opt for double-quote around and replace
1244         // inner double-quotes.
1245         attrValue = attrValue.replace("\"", "&quot;");  //$NON-NLS-1$ //$NON-NLS-2$
1246         return '"' + attrValue + '"';
1247     }
1248 
1249     /**
1250      * Computes the changes to be made to Java file(s) and returns a list of {@link Change}.
1251      */
1252     private List<Change> computeJavaChanges(ICompilationUnit unit,
1253             String xmlStringId,
1254             String tokenString,
1255             RefactoringStatus status,
1256             SubMonitor subMonitor) {
1257 
1258         // Get the Android package name from the Android Manifest. We need it to create
1259         // the FQCN of the R class.
1260         String packageName = null;
1261         String error = null;
1262         IResource manifestFile = mProject.findMember(AndroidConstants.FN_ANDROID_MANIFEST);
1263         if (manifestFile == null || manifestFile.getType() != IResource.FILE) {
1264             error = "File not found";
1265         } else {
1266             try {
1267                 AndroidManifestParser manifest = AndroidManifestParser.parseForData(
1268                         (IFile) manifestFile);
1269                 if (manifest == null) {
1270                     error = "Invalid content";
1271                 } else {
1272                     packageName = manifest.getPackage();
1273                     if (packageName == null) {
1274                         error = "Missing package definition";
1275                     }
1276                 }
1277             } catch (CoreException e) {
1278                 error = e.getLocalizedMessage();
1279             }
1280         }
1281 
1282         if (error != null) {
1283             status.addFatalError(
1284                     String.format("Failed to parse file %1$s: %2$s.",
1285                             manifestFile == null ? "" : manifestFile.getFullPath(),  //$NON-NLS-1$
1286                             error));
1287             return null;
1288         }
1289 
1290         // TODO in a future version we might want to collect various Java files that
1291         // need to be updated in the same project and process them all together.
1292         // To do that we need to use an ASTRequestor and parser.createASTs, kind of
1293         // like this:
1294         //
1295         // ASTRequestor requestor = new ASTRequestor() {
1296         //    @Override
1297         //    public void acceptAST(ICompilationUnit sourceUnit, CompilationUnit astNode) {
1298         //        super.acceptAST(sourceUnit, astNode);
1299         //        // TODO process astNode
1300         //    }
1301         // };
1302         // ...
1303         // parser.createASTs(compilationUnits, bindingKeys, requestor, monitor)
1304         //
1305         // and then add multiple TextFileChange to the changes arraylist.
1306 
1307         // Right now the changes array will contain one TextFileChange at most.
1308         ArrayList<Change> changes = new ArrayList<Change>();
1309 
1310         // This is the unit that will be modified.
1311         TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource());
1312         change.setTextType("java"); //$NON-NLS-1$
1313 
1314         // Create an AST for this compilation unit
1315         ASTParser parser = ASTParser.newParser(AST.JLS3);
1316         parser.setProject(unit.getJavaProject());
1317         parser.setSource(unit);
1318         parser.setResolveBindings(true);
1319         ASTNode node = parser.createAST(subMonitor.newChild(1));
1320 
1321         // The ASTNode must be a CompilationUnit, by design
1322         if (!(node instanceof CompilationUnit)) {
1323             status.addFatalError(String.format("Internal error: ASTNode class %s",  //$NON-NLS-1$
1324                     node.getClass()));
1325             return null;
1326         }
1327 
1328         // ImportRewrite will allow us to add the new type to the imports and will resolve
1329         // what the Java source must reference, e.g. the FQCN or just the simple name.
1330         ImportRewrite importRewrite = ImportRewrite.create((CompilationUnit) node, true);
1331         String Rqualifier = packageName + ".R"; //$NON-NLS-1$
1332         Rqualifier = importRewrite.addImport(Rqualifier);
1333 
1334         // Rewrite the AST itself via an ASTVisitor
1335         AST ast = node.getAST();
1336         ASTRewrite astRewrite = ASTRewrite.create(ast);
1337         ArrayList<TextEditGroup> astEditGroups = new ArrayList<TextEditGroup>();
1338         ReplaceStringsVisitor visitor = new ReplaceStringsVisitor(
1339                 ast, astRewrite, astEditGroups,
1340                 tokenString, Rqualifier, xmlStringId);
1341         node.accept(visitor);
1342 
1343         // Finally prepare the change set
1344         try {
1345             MultiTextEdit edit = new MultiTextEdit();
1346 
1347             // Create the edit to change the imports, only if anything changed
1348             TextEdit subEdit = importRewrite.rewriteImports(subMonitor.newChild(1));
1349             if (subEdit.hasChildren()) {
1350                 edit.addChild(subEdit);
1351             }
1352 
1353             // Create the edit to change the Java source, only if anything changed
1354             subEdit = astRewrite.rewriteAST();
1355             if (subEdit.hasChildren()) {
1356                 edit.addChild(subEdit);
1357             }
1358 
1359             // Only create a change set if any edit was collected
1360             if (edit.hasChildren()) {
1361                 change.setEdit(edit);
1362 
1363                 // Create TextEditChangeGroups which let the user turn changes on or off
1364                 // individually. This must be done after the change.setEdit() call above.
1365                 for (TextEditGroup editGroup : astEditGroups) {
1366                     change.addTextEditChangeGroup(new TextEditChangeGroup(change, editGroup));
1367                 }
1368 
1369                 changes.add(change);
1370             }
1371 
1372             // TODO to modify another Java source, loop back to the creation of the
1373             // TextFileChange and accumulate in changes. Right now only one source is
1374             // modified.
1375 
1376             subMonitor.worked(1);
1377 
1378             if (changes.size() > 0) {
1379                 return changes;
1380             }
1381 
1382         } catch (CoreException e) {
1383             // ImportRewrite.rewriteImports failed.
1384             status.addFatalError(e.getMessage());
1385         }
1386         return null;
1387     }
1388 
1389     /**
1390      * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the
1391      * work and creates a descriptor that can be used to replay that refactoring later.
1392      *
1393      * @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)
1394      *
1395      * @throws CoreException
1396      */
1397     @Override
1398     public Change createChange(IProgressMonitor monitor)
1399             throws CoreException, OperationCanceledException {
1400 
1401         try {
1402             monitor.beginTask("Applying changes...", 1);
1403 
1404             CompositeChange change = new CompositeChange(
1405                     getName(),
1406                     mChanges.toArray(new Change[mChanges.size()])) {
1407                 @Override
1408                 public ChangeDescriptor getDescriptor() {
1409 
1410                     String comment = String.format(
1411                             "Extracts string '%1$s' into R.string.%2$s",
1412                             mTokenString,
1413                             mXmlStringId);
1414 
1415                     ExtractStringDescriptor desc = new ExtractStringDescriptor(
1416                             mProject.getName(), //project
1417                             comment, //description
1418                             comment, //comment
1419                             createArgumentMap());
1420 
1421                     return new RefactoringChangeDescriptor(desc);
1422                 }
1423             };
1424 
1425             monitor.worked(1);
1426 
1427             return change;
1428 
1429         } finally {
1430             monitor.done();
1431         }
1432 
1433     }
1434 
1435     /**
1436      * Given a file project path, returns its resource in the same project than the
1437      * compilation unit. The resource may not exist.
1438      */
1439     private IResource getTargetXmlResource(String xmlFileWsPath) {
1440         IResource resource = mProject.getFile(xmlFileWsPath);
1441         return resource;
1442     }
1443 
1444     /**
1445      * Sets the replacement string ID. Used by the wizard to set the user input.
1446      */
1447     public void setNewStringId(String newStringId) {
1448         mXmlStringId = newStringId;
1449     }
1450 
1451     /**
1452      * Sets the replacement string ID. Used by the wizard to set the user input.
1453      */
1454     public void setNewStringValue(String newStringValue) {
1455         mXmlStringValue = newStringValue;
1456     }
1457 
1458     /**
1459      * Sets the target file. This is a project path, e.g. "/res/values/strings.xml".
1460      * Used by the wizard to set the user input.
1461      */
1462     public void setTargetFile(String targetXmlFileWsPath) {
1463         mTargetXmlFileWsPath = targetXmlFileWsPath;
1464     }
1465 
1466 }
1467