Home | History | Annotate | Download | only in extractstring
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.refactorings.extractstring;
     18 
     19 import static com.android.ide.common.layout.LayoutConstants.STRING_PREFIX;
     20 
     21 import com.android.ide.eclipse.adt.AdtConstants;
     22 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     23 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     24 import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor;
     25 import com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors;
     26 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
     27 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     28 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
     29 import com.android.resources.ResourceFolderType;
     30 import com.android.resources.ResourceType;
     31 import com.android.sdklib.SdkConstants;
     32 import com.android.sdklib.xml.ManifestData;
     33 
     34 import org.eclipse.core.resources.IContainer;
     35 import org.eclipse.core.resources.IFile;
     36 import org.eclipse.core.resources.IFolder;
     37 import org.eclipse.core.resources.IProject;
     38 import org.eclipse.core.resources.IResource;
     39 import org.eclipse.core.resources.ResourceAttributes;
     40 import org.eclipse.core.resources.ResourcesPlugin;
     41 import org.eclipse.core.runtime.CoreException;
     42 import org.eclipse.core.runtime.IPath;
     43 import org.eclipse.core.runtime.IProgressMonitor;
     44 import org.eclipse.core.runtime.OperationCanceledException;
     45 import org.eclipse.core.runtime.Path;
     46 import org.eclipse.core.runtime.SubMonitor;
     47 import org.eclipse.jdt.core.IBuffer;
     48 import org.eclipse.jdt.core.ICompilationUnit;
     49 import org.eclipse.jdt.core.IJavaProject;
     50 import org.eclipse.jdt.core.IPackageFragment;
     51 import org.eclipse.jdt.core.IPackageFragmentRoot;
     52 import org.eclipse.jdt.core.JavaCore;
     53 import org.eclipse.jdt.core.JavaModelException;
     54 import org.eclipse.jdt.core.ToolFactory;
     55 import org.eclipse.jdt.core.compiler.IScanner;
     56 import org.eclipse.jdt.core.compiler.ITerminalSymbols;
     57 import org.eclipse.jdt.core.compiler.InvalidInputException;
     58 import org.eclipse.jdt.core.dom.AST;
     59 import org.eclipse.jdt.core.dom.ASTNode;
     60 import org.eclipse.jdt.core.dom.ASTParser;
     61 import org.eclipse.jdt.core.dom.CompilationUnit;
     62 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
     63 import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
     64 import org.eclipse.jface.text.ITextSelection;
     65 import org.eclipse.ltk.core.refactoring.Change;
     66 import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
     67 import org.eclipse.ltk.core.refactoring.CompositeChange;
     68 import org.eclipse.ltk.core.refactoring.Refactoring;
     69 import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
     70 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
     71 import org.eclipse.ltk.core.refactoring.TextEditChangeGroup;
     72 import org.eclipse.ltk.core.refactoring.TextFileChange;
     73 import org.eclipse.text.edits.InsertEdit;
     74 import org.eclipse.text.edits.MultiTextEdit;
     75 import org.eclipse.text.edits.ReplaceEdit;
     76 import org.eclipse.text.edits.TextEdit;
     77 import org.eclipse.text.edits.TextEditGroup;
     78 import org.eclipse.ui.IEditorPart;
     79 import org.eclipse.wst.sse.core.StructuredModelManager;
     80 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
     81 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     82 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     83 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
     84 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
     85 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
     86 import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
     87 import org.w3c.dom.Node;
     88 
     89 import java.io.IOException;
     90 import java.util.ArrayList;
     91 import java.util.Arrays;
     92 import java.util.HashMap;
     93 import java.util.HashSet;
     94 import java.util.Iterator;
     95 import java.util.LinkedList;
     96 import java.util.List;
     97 import java.util.Map;
     98 import java.util.Queue;
     99 
    100 /**
    101  * This refactoring extracts a string from a file and replaces it by an Android resource ID
    102  * such as R.string.foo.
    103  * <p/>
    104  * There are a number of scenarios, which are not all supported yet. The workflow works as
    105  * such:
    106  * <ul>
    107  * <li> User selects a string in a Java and invokes the {@link ExtractStringAction}.
    108  * <li> The action finds the {@link ICompilationUnit} being edited as well as the current
    109  *      {@link ITextSelection}. The action creates a new instance of this refactoring as
    110  *      well as an {@link ExtractStringWizard} and runs the operation.
    111  * <li> Step 1 of the refactoring is to check the preliminary conditions. Right now we check
    112  *      that the java source is not read-only and is in sync. We also try to find a string under
    113  *      the selection. If this fails, the refactoring is aborted.
    114  * <li> On success, the wizard is shown, which lets the user input the new ID to use.
    115  * <li> The wizard sets the user input values into this refactoring instance, e.g. the new string
    116  *      ID, the XML file to update, etc. The wizard does use the utility method
    117  *      {@link XmlStringFileHelper#valueOfStringId(IProject, String, String)} to check whether
    118  *      the new ID is already defined in the target XML file.
    119  * <li> Once Preview or Finish is selected in the wizard, the
    120  *      {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input
    121  *      and compute the actual changes.
    122  * <li> When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked.
    123  * </ul>
    124  *
    125  * The list of changes are:
    126  * <ul>
    127  * <li> If the target XML does not exist, create it with the new string ID.
    128  * <li> If the target XML exists, find the <resources> node and add the new string ID right after.
    129  *      If the node is <resources/>, it needs to be opened.
    130  * <li> Create an AST rewriter to edit the source Java file and replace all occurrences by the
    131  *      new computed R.string.foo. Also need to rewrite imports to import R as needed.
    132  *      If there's already a conflicting R included, we need to insert the FQCN instead.
    133  * <li> TODO: Have a pref in the wizard: [x] Change other XML Files
    134  * <li> TODO: Have a pref in the wizard: [x] Change other Java Files
    135  * </ul>
    136  */
    137 @SuppressWarnings("restriction")
    138 public class ExtractStringRefactoring extends Refactoring {
    139 
    140     public enum Mode {
    141         /**
    142          * the Extract String refactoring is called on an <em>existing</em> source file.
    143          * Its purpose is then to get the selected string of the source and propose to
    144          * change it by an XML id. The XML id may be a new one or an existing one.
    145          */
    146         EDIT_SOURCE,
    147         /**
    148          * The Extract String refactoring is called without any source file.
    149          * Its purpose is then to create a new XML string ID or select/modify an existing one.
    150          */
    151         SELECT_ID,
    152         /**
    153          * The Extract String refactoring is called without any source file.
    154          * Its purpose is then to create a new XML string ID. The ID must not already exist.
    155          */
    156         SELECT_NEW_ID
    157     }
    158 
    159     /** The {@link Mode} of operation of the refactoring. */
    160     private final Mode mMode;
    161     /** Non-null when editing an Android Resource XML file: identifies the attribute name
    162      * of the value being edited. When null, the source is an Android Java file. */
    163     private String mXmlAttributeName;
    164     /** The file model being manipulated.
    165      * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */
    166     private final IFile mFile;
    167     /** The editor. Non-null when invoked from {@link ExtractStringAction}. Null otherwise. */
    168     private final IEditorPart mEditor;
    169     /** The project that contains {@link #mFile} and that contains the target XML file to modify. */
    170     private final IProject mProject;
    171     /** The start of the selection in {@link #mFile}.
    172      * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */
    173     private final int mSelectionStart;
    174     /** The end of the selection in {@link #mFile}.
    175      * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */
    176     private final int mSelectionEnd;
    177 
    178     /** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */
    179     private ICompilationUnit mUnit;
    180     /** The actual string selected, after UTF characters have been escaped, good for display.
    181      * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */
    182     private String mTokenString;
    183 
    184     /** The XML string ID selected by the user in the wizard. */
    185     private String mXmlStringId;
    186     /** The XML string value. Might be different than the initial selected string. */
    187     private String mXmlStringValue;
    188     /** The path of the XML file that will define {@link #mXmlStringId}, selected by the user
    189      *  in the wizard. This is relative to the project, e.g. "/res/values/string.xml" */
    190     private String mTargetXmlFileWsPath;
    191     /** True if we should find & replace in all Java files. */
    192     private boolean mReplaceAllJava;
    193     /** True if we should find & replace in all XML files of the same name in other res configs
    194      * (other than the main {@link #mTargetXmlFileWsPath}.) */
    195     private boolean mReplaceAllXml;
    196 
    197     /** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and
    198      *  used by {@link #createChange(IProgressMonitor)}. */
    199     private ArrayList<Change> mChanges;
    200 
    201     private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper();
    202 
    203     private static final String KEY_MODE = "mode";                      //$NON-NLS-1$
    204     private static final String KEY_FILE = "file";                      //$NON-NLS-1$
    205     private static final String KEY_PROJECT = "proj";                   //$NON-NLS-1$
    206     private static final String KEY_SEL_START = "sel-start";            //$NON-NLS-1$
    207     private static final String KEY_SEL_END = "sel-end";                //$NON-NLS-1$
    208     private static final String KEY_TOK_ESC = "tok-esc";                //$NON-NLS-1$
    209     private static final String KEY_XML_ATTR_NAME = "xml-attr-name";    //$NON-NLS-1$
    210     private static final String KEY_RPLC_ALL_JAVA = "rplc-all-java";    //$NON-NLS-1$
    211     private static final String KEY_RPLC_ALL_XML  = "rplc-all-xml";     //$NON-NLS-1$
    212 
    213     /**
    214      * This constructor is solely used by {@link ExtractStringDescriptor},
    215      * to replay a previous refactoring.
    216      * <p/>
    217      * To create a refactoring from code, please use one of the two other constructors.
    218      *
    219      * @param arguments A map previously created using {@link #createArgumentMap()}.
    220      * @throws NullPointerException
    221      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    321     public void setTargetFile(String targetXmlFileWsPath) {
    322         mTargetXmlFileWsPath = targetXmlFileWsPath;
    323     }
    324 
    325     public void setReplaceAllJava(boolean replaceAllJava) {
    326         mReplaceAllJava = replaceAllJava;
    327     }
    328 
    329     public void setReplaceAllXml(boolean replaceAllXml) {
    330         mReplaceAllXml = replaceAllXml;
    331     }
    332 
    333     /**
    334      * @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
    335      */
    336     @Override
    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 
    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      */
    355     public String getTokenString() {
    356         return mTokenString;
    357     }
    358 
    359     /** Returns the XML string ID selected by the user in the wizard. */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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      */
   1000     private Iterable<IFile> findAllResXmlFiles() {
   1001         return new Iterable<IFile>() {
   1002             public Iterator<IFile> iterator() {
   1003                 return new Iterator<IFile>() {
   1004                     final Queue<IFile> mFiles = new LinkedList<IFile>();
   1005                     final Queue<IResource> mFolders = new LinkedList<IResource>();
   1006                     IPath mFilterPath1 = null;
   1007                     IPath mFilterPath2 = null;
   1008                     {
   1009                         // Filter out the XML file where we'll be writing the XML string id.
   1010                         IResource filterRes = mProject.findMember(mTargetXmlFileWsPath);
   1011                         if (filterRes != null) {
   1012                             mFilterPath1 = filterRes.getFullPath();
   1013                         }
   1014                         // Filter out the XML source file, if any (e.g. typically a layout)
   1015                         if (mFile != null) {
   1016                             mFilterPath2 = mFile.getFullPath();
   1017                         }
   1018 
   1019                         // We want to process the manifest
   1020                         IResource man = mProject.findMember("AndroidManifest.xml"); // TODO find a constant
   1021                         if (man.exists() && man instanceof IFile && !man.equals(mFile)) {
   1022                             mFiles.add((IFile) man);
   1023                         }
   1024 
   1025                         // Add all /res folders (technically we don't need to process /res/values
   1026                         // XML files that contain resources/string elements, but it's easier to
   1027                         // not filter them out.)
   1028                         IFolder f = mProject.getFolder(AdtConstants.WS_RESOURCES);
   1029                         if (f.exists()) {
   1030                             try {
   1031                                 mFolders.addAll(
   1032                                         Arrays.asList(f.members(IContainer.EXCLUDE_DERIVED)));
   1033                             } catch (CoreException e) {
   1034                                 // pass
   1035                             }
   1036                         }
   1037                     }
   1038 
   1039                     public boolean hasNext() {
   1040                         if (!mFiles.isEmpty()) {
   1041                             return true;
   1042                         }
   1043 
   1044                         while (!mFolders.isEmpty()) {
   1045                             IResource res = mFolders.poll();
   1046                             if (res.exists() && res instanceof IFolder) {
   1047                                 IFolder f = (IFolder) res;
   1048                                 try {
   1049                                     getFileList(f);
   1050                                     if (!mFiles.isEmpty()) {
   1051                                         return true;
   1052                                     }
   1053                                 } catch (CoreException e) {
   1054                                     // pass
   1055                                 }
   1056                             }
   1057                         }
   1058                         return false;
   1059                     }
   1060 
   1061                     private void getFileList(IFolder folder) throws CoreException {
   1062                         for (IResource res : folder.members(IContainer.EXCLUDE_DERIVED)) {
   1063                             // Only accept file resources which are not derived and actually exist
   1064                             if (res.exists() && !res.isDerived() && res instanceof IFile) {
   1065                                 IFile file = (IFile) res;
   1066                                 // Must have an XML extension
   1067                                 if (AdtConstants.EXT_XML.equals(file.getFileExtension())) {
   1068                                     IPath p = file.getFullPath();
   1069                                     // And not be either paths we want to filter out
   1070                                     if ((mFilterPath1 != null && mFilterPath1.equals(p)) ||
   1071                                             (mFilterPath2 != null && mFilterPath2.equals(p))) {
   1072                                         continue;
   1073                                     }
   1074                                     mFiles.add(file);
   1075                                 }
   1076                             }
   1077                         }
   1078                     }
   1079 
   1080                     public IFile next() {
   1081                         IFile file = mFiles.poll();
   1082                         hasNext();
   1083                         return file;
   1084                     }
   1085 
   1086                     public void remove() {
   1087                         throw new UnsupportedOperationException(
   1088                             "This iterator does not support removal");  //$NON-NLS-1$
   1089                     }
   1090                 };
   1091             }
   1092         };
   1093     }
   1094 
   1095     /**
   1096      * Internal helper that actually prepares the {@link Change} that adds the given
   1097      * ID to the given XML File.
   1098      * <p/>
   1099      * This does not actually modify the file.
   1100      *
   1101      * @param targetXml The file resource to modify.
   1102      * @param xmlStringId The new ID to insert.
   1103      * @param tokenString The old string, which will be the value in the XML string.
   1104      * @return A new {@link TextEdit} that describes how to change the file.
   1105      */
   1106     private Change createXmlChanges(IFile targetXml,
   1107             String xmlStringId,
   1108             String tokenString,
   1109             RefactoringStatus status,
   1110             SubMonitor monitor) {
   1111 
   1112         TextFileChange xmlChange = new TextFileChange(getName(), targetXml);
   1113         xmlChange.setTextType(AdtConstants.EXT_XML);
   1114 
   1115         String error = "";                  //$NON-NLS-1$
   1116         TextEdit edit = null;
   1117         TextEditGroup editGroup = null;
   1118 
   1119         try {
   1120             if (!targetXml.exists()) {
   1121                 // Kludge: use targetXml==null as a signal this is a new file being created
   1122                 targetXml = null;
   1123             }
   1124 
   1125             edit = createXmlReplaceEdit(targetXml, xmlStringId, tokenString, status,
   1126                     SubMonitor.convert(monitor, 1));
   1127         } catch (IOException e) {
   1128             error = e.toString();
   1129         } catch (CoreException e) {
   1130             // Failed to read file. Ignore. Will handle error below.
   1131             error = e.toString();
   1132         }
   1133 
   1134         if (edit == null) {
   1135             status.addFatalError(String.format("Failed to modify file %1$s%2$s",
   1136                     targetXml == null ? "" : targetXml.getFullPath(),   //$NON-NLS-1$
   1137                     error == null ? "" : ": " + error));                //$NON-NLS-1$
   1138             return null;
   1139         }
   1140 
   1141         editGroup = new TextEditGroup(targetXml == null ? "Create <string> in new XML file"
   1142                                                         : "Insert <string> in XML file",
   1143                                       edit);
   1144 
   1145         xmlChange.setEdit(edit);
   1146         // The TextEditChangeGroup let the user toggle this change on and off later.
   1147         xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup));
   1148 
   1149         monitor.worked(1);
   1150         return xmlChange;
   1151     }
   1152 
   1153     /**
   1154      * Scan the XML file to find the best place where to insert the new string element.
   1155      * <p/>
   1156      * This handles a variety of cases, including replacing existing ids in place,
   1157      * adding the top resources element if missing and the XML PI if not present.
   1158      * It tries to preserve indentation when adding new elements at the end of an existing XML.
   1159      *
   1160      * @param file The XML file to modify, that must be present in the workspace.
   1161      *             Pass null to create a change for a new file that doesn't exist yet.
   1162      * @param xmlStringId The new ID to insert.
   1163      * @param tokenString The old string, which will be the value in the XML string.
   1164      * @param status The in-out refactoring status. Used to log a more detailed error if the
   1165      *          XML has a top element that is not a resources element.
   1166      * @param monitor A monitor to track progress.
   1167      * @return A new {@link TextEdit} for either a replace or an insert operation, or null in case
   1168      *          of error.
   1169      * @throws CoreException - if the file's contents or description can not be read.
   1170      * @throws IOException   - if the file's contents can not be read or its detected encoding does
   1171      *                         not support its contents.
   1172      */
   1173     private TextEdit createXmlReplaceEdit(IFile file,
   1174             String xmlStringId,
   1175             String tokenString,
   1176             RefactoringStatus status,
   1177             SubMonitor monitor)
   1178                 throws IOException, CoreException {
   1179 
   1180         IModelManager modelMan = StructuredModelManager.getModelManager();
   1181 
   1182         final String NODE_RESOURCES = ResourcesDescriptors.ROOT_ELEMENT;
   1183         final String NODE_STRING = "string";    //$NON-NLS-1$ //TODO find or create constant
   1184         final String ATTR_NAME = "name";        //$NON-NLS-1$ //TODO find or create constant
   1185 
   1186 
   1187         // Scan the source to find the best insertion point.
   1188 
   1189         // 1- The most common case we need to handle is the one of inserting at the end
   1190         //    of a valid XML document, respecting the whitespace last used.
   1191         //
   1192         // Ideally we have this structure:
   1193         // <xml ...>
   1194         // <resource>
   1195         // ...ws1...<string>blah</string>...ws2...
   1196         // </resource>
   1197         //
   1198         // where ws1 and ws2 are the whitespace respectively before and after the last element
   1199         // just before the closing </resource>.
   1200         // In this case we want to generate the new string just before ws2...</resource> with
   1201         // the same whitespace as ws1.
   1202         //
   1203         // 2- Another expected case is there's already an existing string which "name" attribute
   1204         //    equals to xmlStringId and we just want to replace its value.
   1205         //
   1206         // Other cases we need to handle:
   1207         // 3- There is no element at all -> create a full new <resource>+<string> content.
   1208         // 4- There is <resource/>, that is the tag is not opened. This can be handled as the
   1209         //    previous case, generating full content but also replacing <resource/>.
   1210         // 5- There is a top element that is not <resource>. That's a fatal error and we abort.
   1211 
   1212         IStructuredModel smodel = null;
   1213 
   1214         // Single and double quotes must be escaped in the <string>value</string> declaration
   1215         tokenString = escapeString(tokenString);
   1216 
   1217         try {
   1218             IStructuredDocument sdoc = null;
   1219             boolean checkTopElement = true;
   1220             boolean replaceStringContent = false;
   1221             boolean hasPiXml = false;
   1222             int newResStart = 0;
   1223             int newResLength = 0;
   1224             String lineSep = "\n";                  //$NON-NLS-1$
   1225 
   1226             if (file != null) {
   1227                 smodel = modelMan.getExistingModelForRead(file);
   1228                 if (smodel != null) {
   1229                     sdoc = smodel.getStructuredDocument();
   1230                 } else if (smodel == null) {
   1231                     // The model is not currently open.
   1232                     if (file.exists()) {
   1233                         sdoc = modelMan.createStructuredDocumentFor(file);
   1234                     } else {
   1235                         sdoc = modelMan.createNewStructuredDocumentFor(file);
   1236                     }
   1237                 }
   1238             }
   1239 
   1240             if (sdoc == null && file != null) {
   1241                 // Get a document matching the actual saved file
   1242                 sdoc = modelMan.createStructuredDocumentFor(file);
   1243             }
   1244 
   1245             if (sdoc != null) {
   1246                 String wsBefore = "";   //$NON-NLS-1$
   1247                 String lastWs = null;
   1248 
   1249                 lineSep = sdoc.getLineDelimiter();
   1250                 if (lineSep == null || lineSep.length() == 0) {
   1251                     // That wasn't too useful, let's go back to a reasonable default
   1252                     lineSep = "\n"; //$NON-NLS-1$
   1253                 }
   1254 
   1255                 for (IStructuredDocumentRegion regions : sdoc.getStructuredDocumentRegions()) {
   1256                     String type = regions.getType();
   1257 
   1258                     if (DOMRegionContext.XML_CONTENT.equals(type)) {
   1259 
   1260                         if (replaceStringContent) {
   1261                             // Generate a replacement for a <string> value matching the string ID.
   1262                             return new ReplaceEdit(
   1263                                     regions.getStartOffset(), regions.getLength(), tokenString);
   1264                         }
   1265 
   1266                         // Otherwise capture what should be whitespace content
   1267                         lastWs = regions.getFullText();
   1268                         continue;
   1269 
   1270                     } else if (DOMRegionContext.XML_PI_OPEN.equals(type) && !hasPiXml) {
   1271 
   1272                         int nb = regions.getNumberOfRegions();
   1273                         ITextRegionList list = regions.getRegions();
   1274                         for (int i = 0; i < nb; i++) {
   1275                             ITextRegion region = list.get(i);
   1276                             type = region.getType();
   1277                             if (DOMRegionContext.XML_TAG_NAME.equals(type)) {
   1278                                 String name = regions.getText(region);
   1279                                 if ("xml".equals(name)) {   //$NON-NLS-1$
   1280                                     hasPiXml = true;
   1281                                     break;
   1282                                 }
   1283                             }
   1284                         }
   1285                         continue;
   1286 
   1287                     } else if (!DOMRegionContext.XML_TAG_NAME.equals(type)) {
   1288                         // ignore things which are not a tag nor text content (such as comments)
   1289                         continue;
   1290                     }
   1291 
   1292                     int nb = regions.getNumberOfRegions();
   1293                     ITextRegionList list = regions.getRegions();
   1294 
   1295                     String name = null;
   1296                     String attrName = null;
   1297                     String attrValue = null;
   1298                     boolean isEmptyTag = false;
   1299                     boolean isCloseTag = false;
   1300 
   1301                     for (int i = 0; i < nb; i++) {
   1302                         ITextRegion region = list.get(i);
   1303                         type = region.getType();
   1304 
   1305                         if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) {
   1306                             isCloseTag = true;
   1307                         } else if (DOMRegionContext.XML_EMPTY_TAG_CLOSE.equals(type)) {
   1308                             isEmptyTag = true;
   1309                         } else if (DOMRegionContext.XML_TAG_NAME.equals(type)) {
   1310                             name = regions.getText(region);
   1311                         } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type) &&
   1312                                 NODE_STRING.equals(name)) {
   1313                             // Record the attribute names into a <string> element.
   1314                             attrName = regions.getText(region);
   1315                         } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type) &&
   1316                                 ATTR_NAME.equals(attrName)) {
   1317                             // Record the value of a <string name=...> attribute
   1318                             attrValue = regions.getText(region);
   1319 
   1320                             if (attrValue != null &&
   1321                                     unquoteAttrValue(attrValue).equals(xmlStringId)) {
   1322                                 // We found a <string name=> matching the string ID to replace.
   1323                                 // We'll generate a replacement when we process the string value
   1324                                 // (that is the next XML_CONTENT region.)
   1325                                 replaceStringContent = true;
   1326                             }
   1327                         }
   1328                     }
   1329 
   1330                     if (checkTopElement) {
   1331                         // Check the top element has a resource name
   1332                         checkTopElement = false;
   1333                         if (!NODE_RESOURCES.equals(name)) {
   1334                             status.addFatalError(
   1335                                     String.format("XML file lacks a <resource> tag: %1$s",
   1336                                             mTargetXmlFileWsPath));
   1337                             return null;
   1338 
   1339                         }
   1340 
   1341                         if (isEmptyTag) {
   1342                             // The top element is an empty "<resource/>" tag. We need to do
   1343                             // a full new resource+string replacement.
   1344                             newResStart = regions.getStartOffset();
   1345                             newResLength = regions.getLength();
   1346                         }
   1347                     }
   1348 
   1349                     if (NODE_RESOURCES.equals(name)) {
   1350                         if (isCloseTag) {
   1351                             // We found the </resource> tag and we want
   1352                             // to insert just before this one.
   1353 
   1354                             StringBuilder content = new StringBuilder();
   1355                             content.append(wsBefore)
   1356                                    .append("<string name=\"")                   //$NON-NLS-1$
   1357                                    .append(xmlStringId)
   1358                                    .append("\">")                               //$NON-NLS-1$
   1359                                    .append(tokenString)
   1360                                    .append("</string>");                        //$NON-NLS-1$
   1361 
   1362                             // Backup to insert before the whitespace preceding </resource>
   1363                             IStructuredDocumentRegion insertBeforeReg = regions;
   1364                             while (true) {
   1365                                 IStructuredDocumentRegion previous = insertBeforeReg.getPrevious();
   1366                                 if (previous != null &&
   1367                                         DOMRegionContext.XML_CONTENT.equals(previous.getType()) &&
   1368                                         previous.getText().trim().length() == 0) {
   1369                                     insertBeforeReg = previous;
   1370                                 } else {
   1371                                     break;
   1372                                 }
   1373                             }
   1374                             if (insertBeforeReg == regions) {
   1375                                 // If we have not found any whitespace before </resources>,
   1376                                 // at least add a line separator.
   1377                                 content.append(lineSep);
   1378                             }
   1379 
   1380                             return new InsertEdit(insertBeforeReg.getStartOffset(),
   1381                                                   content.toString());
   1382                         }
   1383                     } else {
   1384                         // For any other tag than <resource>, capture whitespace before and after.
   1385                         if (!isCloseTag) {
   1386                             wsBefore = lastWs;
   1387                         }
   1388                     }
   1389                 }
   1390             }
   1391 
   1392             // We reach here either because there's no XML content at all or because
   1393             // there's an empty <resource/>.
   1394             // Provide a full new resource+string replacement.
   1395             StringBuilder content = new StringBuilder();
   1396             if (!hasPiXml) {
   1397                 content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); //$NON-NLS-1$
   1398                 content.append(lineSep);
   1399             } else if (newResLength == 0 && sdoc != null) {
   1400                 // If inserting at the end, check if the last region is some whitespace.
   1401                 // If there's no newline, insert one ourselves.
   1402                 IStructuredDocumentRegion lastReg = sdoc.getLastStructuredDocumentRegion();
   1403                 if (lastReg != null && lastReg.getText().indexOf('\n') == -1) {
   1404                     content.append('\n');
   1405                 }
   1406             }
   1407 
   1408             // FIXME how to access formatting preferences to generate the proper indentation?
   1409             content.append("<resources>").append(lineSep);                  //$NON-NLS-1$
   1410             content.append("    <string name=\"")                           //$NON-NLS-1$
   1411                    .append(xmlStringId)
   1412                    .append("\">")                                           //$NON-NLS-1$
   1413                    .append(tokenString)
   1414                    .append("</string>")                                     //$NON-NLS-1$
   1415                    .append(lineSep);
   1416             content.append("</resources>").append(lineSep);                 //$NON-NLS-1$
   1417 
   1418             if (newResLength > 0) {
   1419                 // Replace existing piece
   1420                 return new ReplaceEdit(newResStart, newResLength, content.toString());
   1421             } else {
   1422                 // Insert at the end.
   1423                 int offset = sdoc == null ? 0 : sdoc.getLength();
   1424                 return new InsertEdit(offset, content.toString());
   1425             }
   1426         } catch (IOException e) {
   1427             // This is expected to happen and is properly reported to the UI.
   1428             throw e;
   1429         } catch (CoreException e) {
   1430             // This is expected to happen and is properly reported to the UI.
   1431             throw e;
   1432         } catch (Throwable t) {
   1433             // Since we use some internal APIs, use a broad catch-all to report any
   1434             // unexpected issue rather than crash the whole refactoring.
   1435             status.addFatalError(
   1436                     String.format("XML replace error: %1$s", t.getMessage()));
   1437         } finally {
   1438             if (smodel != null) {
   1439                 smodel.releaseFromRead();
   1440             }
   1441         }
   1442 
   1443         return null;
   1444     }
   1445 
   1446     /**
   1447      * Escape a string value to be placed in a string resource file such that it complies with
   1448      * the escaping rules described here:
   1449      *   http://developer.android.com/guide/topics/resources/string-resource.html
   1450      * More examples of the escaping rules can be found here:
   1451      *   http://androidcookbook.com/Recipe.seam?recipeId=2219&recipeFrom=ViewTOC
   1452      * This method assumes that the String is not escaped already.
   1453      *
   1454      * Rules:
   1455      * <ul>
   1456      * <li>Double quotes are needed if string starts or ends with at least one space.
   1457      * <li>{@code @, ?} at beginning of string have to be escaped with a backslash.
   1458      * <li>{@code ', ", \} have to be escaped with a backslash.
   1459      * <li>{@code <, >, &} have to be replaced by their predefined xml entity.
   1460      * <li>{@code \n, \t} have to be replaced by a backslash and the appropriate character.
   1461      * </ul>
   1462      * @param s the string to be escaped
   1463      * @return the escaped string as it would appear in the XML text in a values file
   1464      */
   1465     public static String escapeString(String s) {
   1466         int n = s.length();
   1467         if (n == 0) {
   1468             return "";
   1469         }
   1470 
   1471         StringBuilder sb = new StringBuilder(s.length() * 2);
   1472         boolean hasSpace = s.charAt(0) == ' ' || s.charAt(n - 1) == ' ';
   1473 
   1474         if (hasSpace) {
   1475             sb.append('"');
   1476         } else if (s.charAt(0) == '@' || s.charAt(0) == '?') {
   1477             sb.append('\\');
   1478         }
   1479 
   1480         for (int i = 0; i < n; ++i) {
   1481             char c = s.charAt(i);
   1482             switch (c) {
   1483                 case '\'':
   1484                     if (!hasSpace) {
   1485                         sb.append('\\');
   1486                     }
   1487                     sb.append(c);
   1488                     break;
   1489                 case '"':
   1490                 case '\\':
   1491                     sb.append('\\');
   1492                     sb.append(c);
   1493                     break;
   1494                 case '<':
   1495                     sb.append("&lt;"); //$NON-NLS-1$
   1496                     break;
   1497                 case '&':
   1498                     sb.append("&amp;"); //$NON-NLS-1$
   1499                     break;
   1500                 case '\n':
   1501                     sb.append("\\n"); //$NON-NLS-1$
   1502                     break;
   1503                 case '\t':
   1504                     sb.append("\\t"); //$NON-NLS-1$
   1505                     break;
   1506                 default:
   1507                     sb.append(c);
   1508                     break;
   1509             }
   1510         }
   1511 
   1512         if (hasSpace) {
   1513             sb.append('"');
   1514         }
   1515 
   1516         return sb.toString();
   1517     }
   1518 
   1519     /**
   1520      * Computes the changes to be made to the source Android XML file and
   1521      * returns a list of {@link Change}.
   1522      * <p/>
   1523      * This function scans an XML file, looking for an attribute value equals to
   1524      * <code>tokenString</code>. If non null, <code>xmlAttrName</code> limit the search
   1525      * to only attributes that have that name.
   1526      * If found, a change is made to replace each occurrence of <code>tokenString</code>
   1527      * by a new "@string/..." using the new <code>xmlStringId</code>.
   1528      *
   1529      * @param sourceFile The file to process.
   1530      *          A status error will be generated if it does not exists.
   1531      *          Must not be null.
   1532      * @param tokenString The string to find. Must not be null or empty.
   1533      * @param xmlAttrName Optional attribute name to limit the search. Can be null.
   1534      * @param allConfigurations True if this function should can all XML files with the same
   1535      *          name and the same resource type folder but with different configurations.
   1536      * @param status Status used to report fatal errors.
   1537      * @param monitor Used to log progress.
   1538      */
   1539     private List<Change> computeXmlSourceChanges(IFile sourceFile,
   1540             String xmlStringId,
   1541             String tokenString,
   1542             String xmlAttrName,
   1543             boolean allConfigurations,
   1544             RefactoringStatus status,
   1545             IProgressMonitor monitor) {
   1546 
   1547         if (!sourceFile.exists()) {
   1548             status.addFatalError(String.format("XML file '%1$s' does not exist.",
   1549                     sourceFile.getFullPath().toOSString()));
   1550             return null;
   1551         }
   1552 
   1553         // We shouldn't be trying to replace a null or empty string.
   1554         assert tokenString != null && tokenString.length() > 0;
   1555         if (tokenString == null || tokenString.length() == 0) {
   1556             return null;
   1557         }
   1558 
   1559         // Note: initially this method was only processing files using a pattern
   1560         //   /project/res/<type>-<configuration>/<filename.xml>
   1561         // However the last version made that more generic to be able to process any XML
   1562         // files. We should probably revisit and simplify this later.
   1563         HashSet<IFile> files = new HashSet<IFile>();
   1564         files.add(sourceFile);
   1565 
   1566         if (allConfigurations && AdtConstants.EXT_XML.equals(sourceFile.getFileExtension())) {
   1567             IPath path = sourceFile.getFullPath();
   1568             if (path.segmentCount() == 4 && path.segment(1).equals(SdkConstants.FD_RESOURCES)) {
   1569                 IProject project = sourceFile.getProject();
   1570                 String filename = path.segment(3);
   1571                 String initialTypeName = path.segment(2);
   1572                 ResourceFolderType type = ResourceFolderType.getFolderType(initialTypeName);
   1573 
   1574                 IContainer res = sourceFile.getParent().getParent();
   1575                 if (type != null && res != null && res.getType() == IResource.FOLDER) {
   1576                     try {
   1577                         for (IResource r : res.members()) {
   1578                             if (r != null && r.getType() == IResource.FOLDER) {
   1579                                 String name = r.getName();
   1580                                 // Skip the initial folder name, it's already in the list.
   1581                                 if (!name.equals(initialTypeName)) {
   1582                                     // Only accept the same folder type (e.g. layout-*)
   1583                                     ResourceFolderType t =
   1584                                         ResourceFolderType.getFolderType(name);
   1585                                     if (type.equals(t)) {
   1586                                         // recompute the path
   1587                                         IPath p = res.getProjectRelativePath().append(name).
   1588                                                                                append(filename);
   1589                                         IResource f = project.findMember(p);
   1590                                         if (f != null && f instanceof IFile) {
   1591                                             files.add((IFile) f);
   1592                                         }
   1593                                     }
   1594                                 }
   1595                             }
   1596                         }
   1597                     } catch (CoreException e) {
   1598                         // Ignore.
   1599                     }
   1600                 }
   1601             }
   1602         }
   1603 
   1604         SubMonitor subMonitor = SubMonitor.convert(monitor, Math.min(1, files.size()));
   1605 
   1606         ArrayList<Change> changes = new ArrayList<Change>();
   1607 
   1608         // Portability note: getModelManager is part of wst.sse.core however the
   1609         // interface returned is part of wst.sse.core.internal.provisional so we can
   1610         // expect it to change in a distant future if they start cleaning their codebase,
   1611         // however unlikely that is.
   1612         IModelManager modelManager = StructuredModelManager.getModelManager();
   1613 
   1614         for (IFile file : files) {
   1615 
   1616             IStructuredModel smodel = null;
   1617             MultiTextEdit multiEdit = null;
   1618             TextFileChange xmlChange = null;
   1619             ArrayList<TextEditGroup> editGroups = null;
   1620 
   1621             try {
   1622                 IStructuredDocument sdoc = null;
   1623 
   1624                 smodel = modelManager.getExistingModelForRead(file);
   1625                 if (smodel != null) {
   1626                     sdoc = smodel.getStructuredDocument();
   1627                 } else if (smodel == null) {
   1628                     // The model is not currently open.
   1629                     if (file.exists()) {
   1630                         sdoc = modelManager.createStructuredDocumentFor(file);
   1631                     } else {
   1632                         sdoc = modelManager.createNewStructuredDocumentFor(file);
   1633                     }
   1634                 }
   1635 
   1636                 if (sdoc == null) {
   1637                     status.addFatalError("XML structured document not found");     //$NON-NLS-1$
   1638                     continue;
   1639                 }
   1640 
   1641                 multiEdit = new MultiTextEdit();
   1642                 editGroups = new ArrayList<TextEditGroup>();
   1643                 xmlChange = new TextFileChange(getName(), file);
   1644                 xmlChange.setTextType("xml");   //$NON-NLS-1$
   1645 
   1646                 String quotedReplacement = quotedAttrValue(STRING_PREFIX + xmlStringId);
   1647 
   1648                 // Prepare the change set
   1649                 for (IStructuredDocumentRegion regions : sdoc.getStructuredDocumentRegions()) {
   1650                     // Only look at XML "top regions"
   1651                     if (!DOMRegionContext.XML_TAG_NAME.equals(regions.getType())) {
   1652                         continue;
   1653                     }
   1654 
   1655                     int nb = regions.getNumberOfRegions();
   1656                     ITextRegionList list = regions.getRegions();
   1657                     String lastAttrName = null;
   1658 
   1659                     for (int i = 0; i < nb; i++) {
   1660                         ITextRegion subRegion = list.get(i);
   1661                         String type = subRegion.getType();
   1662 
   1663                         if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
   1664                             // Memorize the last attribute name seen
   1665                             lastAttrName = regions.getText(subRegion);
   1666 
   1667                         } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
   1668                             // Check this is the attribute and the original string
   1669                             String text = regions.getText(subRegion);
   1670 
   1671                             // Remove " or ' quoting present in the attribute value
   1672                             text = unquoteAttrValue(text);
   1673 
   1674                             if (tokenString.equals(text) &&
   1675                                     (xmlAttrName == null || xmlAttrName.equals(lastAttrName))) {
   1676 
   1677                                 // Found an occurrence. Create a change for it.
   1678                                 TextEdit edit = new ReplaceEdit(
   1679                                         regions.getStartOffset() + subRegion.getStart(),
   1680                                         subRegion.getTextLength(),
   1681                                         quotedReplacement);
   1682                                 TextEditGroup editGroup = new TextEditGroup(
   1683                                         "Replace attribute string by ID",
   1684                                         edit);
   1685 
   1686                                 multiEdit.addChild(edit);
   1687                                 editGroups.add(editGroup);
   1688                             }
   1689                         }
   1690                     }
   1691                 }
   1692             } catch (Throwable t) {
   1693                 // Since we use some internal APIs, use a broad catch-all to report any
   1694                 // unexpected issue rather than crash the whole refactoring.
   1695                 status.addFatalError(
   1696                         String.format("XML refactoring error: %1$s", t.getMessage()));
   1697             } finally {
   1698                 if (smodel != null) {
   1699                     smodel.releaseFromRead();
   1700                 }
   1701 
   1702                 if (multiEdit != null &&
   1703                         xmlChange != null &&
   1704                         editGroups != null &&
   1705                         multiEdit.hasChildren()) {
   1706                     xmlChange.setEdit(multiEdit);
   1707                     for (TextEditGroup group : editGroups) {
   1708                         xmlChange.addTextEditChangeGroup(
   1709                                 new TextEditChangeGroup(xmlChange, group));
   1710                     }
   1711                     changes.add(xmlChange);
   1712                 }
   1713                 subMonitor.worked(1);
   1714             }
   1715         } // for files
   1716 
   1717         if (changes.size() > 0) {
   1718             return changes;
   1719         }
   1720         return null;
   1721     }
   1722 
   1723     /**
   1724      * Returns a quoted attribute value suitable to be placed after an attributeName=
   1725      * statement in an XML stream.
   1726      *
   1727      * According to http://www.w3.org/TR/2008/REC-xml-20081126/#NT-AttValue
   1728      * the attribute value can be either quoted using ' or " and the corresponding
   1729      * entities &apos; or &quot; must be used inside.
   1730      */
   1731     private String quotedAttrValue(String attrValue) {
   1732         if (attrValue.indexOf('"') == -1) {
   1733             // no double-quotes inside, use double-quotes around.
   1734             return '"' + attrValue + '"';
   1735         }
   1736         if (attrValue.indexOf('\'') == -1) {
   1737             // no single-quotes inside, use single-quotes around.
   1738             return '\'' + attrValue + '\'';
   1739         }
   1740         // If we get here, there's a mix. Opt for double-quote around and replace
   1741         // inner double-quotes.
   1742         attrValue = attrValue.replace("\"", "&quot;");  //$NON-NLS-1$ //$NON-NLS-2$
   1743         return '"' + attrValue + '"';
   1744     }
   1745 
   1746     // --- Java changes ---
   1747 
   1748     /**
   1749      * Returns a foreach compatible iterator over all ICompilationUnit in the project.
   1750      */
   1751     private Iterable<ICompilationUnit> findAllJavaUnits() {
   1752         final IJavaProject javaProject = JavaCore.create(mProject);
   1753 
   1754         return new Iterable<ICompilationUnit>() {
   1755             public Iterator<ICompilationUnit> iterator() {
   1756                 return new Iterator<ICompilationUnit>() {
   1757                     final Queue<ICompilationUnit> mUnits = new LinkedList<ICompilationUnit>();
   1758                     final Queue<IPackageFragment> mFragments = new LinkedList<IPackageFragment>();
   1759                     {
   1760                         try {
   1761                             IPackageFragment[] tmpFrags = javaProject.getPackageFragments();
   1762                             if (tmpFrags != null && tmpFrags.length > 0) {
   1763                                 mFragments.addAll(Arrays.asList(tmpFrags));
   1764                             }
   1765                         } catch (JavaModelException e) {
   1766                             // pass
   1767                         }
   1768                     }
   1769 
   1770                     public boolean hasNext() {
   1771                         if (!mUnits.isEmpty()) {
   1772                             return true;
   1773                         }
   1774 
   1775                         while (!mFragments.isEmpty()) {
   1776                             try {
   1777                                 IPackageFragment fragment = mFragments.poll();
   1778                                 if (fragment.getKind() == IPackageFragmentRoot.K_SOURCE) {
   1779                                     ICompilationUnit[] tmpUnits = fragment.getCompilationUnits();
   1780                                     if (tmpUnits != null && tmpUnits.length > 0) {
   1781                                         mUnits.addAll(Arrays.asList(tmpUnits));
   1782                                         return true;
   1783                                     }
   1784                                 }
   1785                             } catch (JavaModelException e) {
   1786                                 // pass
   1787                             }
   1788                         }
   1789                         return false;
   1790                     }
   1791 
   1792                     public ICompilationUnit next() {
   1793                         ICompilationUnit unit = mUnits.poll();
   1794                         hasNext();
   1795                         return unit;
   1796                     }
   1797 
   1798                     public void remove() {
   1799                         throw new UnsupportedOperationException(
   1800                                 "This iterator does not support removal");  //$NON-NLS-1$
   1801                     }
   1802                 };
   1803             }
   1804         };
   1805     }
   1806 
   1807     /**
   1808      * Computes the changes to be made to Java file(s) and returns a list of {@link Change}.
   1809      * <p/>
   1810      * This function scans a Java compilation unit using {@link ReplaceStringsVisitor}, looking
   1811      * for a string literal equals to <code>tokenString</code>.
   1812      * If found, a change is made to replace each occurrence of <code>tokenString</code> by
   1813      * a piece of Java code that somehow accesses R.string.<code>xmlStringId</code>.
   1814      *
   1815      * @param unit The compilated unit to process. Must not be null.
   1816      * @param tokenString The string to find. Must not be null or empty.
   1817      * @param status Status used to report fatal errors.
   1818      * @param monitor Used to log progress.
   1819      */
   1820     private List<Change> computeJavaChanges(ICompilationUnit unit,
   1821             String xmlStringId,
   1822             String tokenString,
   1823             RefactoringStatus status,
   1824             SubMonitor monitor) {
   1825 
   1826         // We shouldn't be trying to replace a null or empty string.
   1827         assert tokenString != null && tokenString.length() > 0;
   1828         if (tokenString == null || tokenString.length() == 0) {
   1829             return null;
   1830         }
   1831 
   1832         // Get the Android package name from the Android Manifest. We need it to create
   1833         // the FQCN of the R class.
   1834         String packageName = null;
   1835         String error = null;
   1836         IResource manifestFile = mProject.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML);
   1837         if (manifestFile == null || manifestFile.getType() != IResource.FILE) {
   1838             error = "File not found";
   1839         } else {
   1840             ManifestData manifestData = AndroidManifestHelper.parseForData((IFile) manifestFile);
   1841             if (manifestData == null) {
   1842                 error = "Invalid content";
   1843             } else {
   1844                 packageName = manifestData.getPackage();
   1845                 if (packageName == null) {
   1846                     error = "Missing package definition";
   1847                 }
   1848             }
   1849         }
   1850 
   1851         if (error != null) {
   1852             status.addFatalError(
   1853                     String.format("Failed to parse file %1$s: %2$s.",
   1854                             manifestFile == null ? "" : manifestFile.getFullPath(),  //$NON-NLS-1$
   1855                             error));
   1856             return null;
   1857         }
   1858 
   1859         // Right now the changes array will contain one TextFileChange at most.
   1860         ArrayList<Change> changes = new ArrayList<Change>();
   1861 
   1862         // This is the unit that will be modified.
   1863         TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource());
   1864         change.setTextType("java"); //$NON-NLS-1$
   1865 
   1866         // Create an AST for this compilation unit
   1867         ASTParser parser = ASTParser.newParser(AST.JLS3);
   1868         parser.setProject(unit.getJavaProject());
   1869         parser.setSource(unit);
   1870         parser.setResolveBindings(true);
   1871         ASTNode node = parser.createAST(monitor.newChild(1));
   1872 
   1873         // The ASTNode must be a CompilationUnit, by design
   1874         if (!(node instanceof CompilationUnit)) {
   1875             status.addFatalError(String.format("Internal error: ASTNode class %s",  //$NON-NLS-1$
   1876                     node.getClass()));
   1877             return null;
   1878         }
   1879 
   1880         // ImportRewrite will allow us to add the new type to the imports and will resolve
   1881         // what the Java source must reference, e.g. the FQCN or just the simple name.
   1882         ImportRewrite importRewrite = ImportRewrite.create((CompilationUnit) node, true);
   1883         String Rqualifier = packageName + ".R"; //$NON-NLS-1$
   1884         Rqualifier = importRewrite.addImport(Rqualifier);
   1885 
   1886         // Rewrite the AST itself via an ASTVisitor
   1887         AST ast = node.getAST();
   1888         ASTRewrite astRewrite = ASTRewrite.create(ast);
   1889         ArrayList<TextEditGroup> astEditGroups = new ArrayList<TextEditGroup>();
   1890         ReplaceStringsVisitor visitor = new ReplaceStringsVisitor(
   1891                 ast, astRewrite, astEditGroups,
   1892                 tokenString, Rqualifier, xmlStringId);
   1893         node.accept(visitor);
   1894 
   1895         // Finally prepare the change set
   1896         try {
   1897             MultiTextEdit edit = new MultiTextEdit();
   1898 
   1899             // Create the edit to change the imports, only if anything changed
   1900             TextEdit subEdit = importRewrite.rewriteImports(monitor.newChild(1));
   1901             if (subEdit.hasChildren()) {
   1902                 edit.addChild(subEdit);
   1903             }
   1904 
   1905             // Create the edit to change the Java source, only if anything changed
   1906             subEdit = astRewrite.rewriteAST();
   1907             if (subEdit.hasChildren()) {
   1908                 edit.addChild(subEdit);
   1909             }
   1910 
   1911             // Only create a change set if any edit was collected
   1912             if (edit.hasChildren()) {
   1913                 change.setEdit(edit);
   1914 
   1915                 // Create TextEditChangeGroups which let the user turn changes on or off
   1916                 // individually. This must be done after the change.setEdit() call above.
   1917                 for (TextEditGroup editGroup : astEditGroups) {
   1918                     TextEditChangeGroup group = new TextEditChangeGroup(change, editGroup);
   1919                     if (editGroup instanceof EnabledTextEditGroup) {
   1920                         group.setEnabled(((EnabledTextEditGroup) editGroup).isEnabled());
   1921                     }
   1922                     change.addTextEditChangeGroup(group);
   1923                 }
   1924 
   1925                 changes.add(change);
   1926             }
   1927 
   1928             monitor.worked(1);
   1929 
   1930             if (changes.size() > 0) {
   1931                 return changes;
   1932             }
   1933 
   1934         } catch (CoreException e) {
   1935             // ImportRewrite.rewriteImports failed.
   1936             status.addFatalError(e.getMessage());
   1937         }
   1938         return null;
   1939     }
   1940 
   1941     // ----
   1942 
   1943     /**
   1944      * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the
   1945      * work and creates a descriptor that can be used to replay that refactoring later.
   1946      *
   1947      * @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)
   1948      *
   1949      * @throws CoreException
   1950      */
   1951     @Override
   1952     public Change createChange(IProgressMonitor monitor)
   1953             throws CoreException, OperationCanceledException {
   1954 
   1955         try {
   1956             monitor.beginTask("Applying changes...", 1);
   1957 
   1958             CompositeChange change = new CompositeChange(
   1959                     getName(),
   1960                     mChanges.toArray(new Change[mChanges.size()])) {
   1961                 @Override
   1962                 public ChangeDescriptor getDescriptor() {
   1963 
   1964                     String comment = String.format(
   1965                             "Extracts string '%1$s' into R.string.%2$s",
   1966                             mTokenString,
   1967                             mXmlStringId);
   1968 
   1969                     ExtractStringDescriptor desc = new ExtractStringDescriptor(
   1970                             mProject.getName(), //project
   1971                             comment, //description
   1972                             comment, //comment
   1973                             createArgumentMap());
   1974 
   1975                     return new RefactoringChangeDescriptor(desc);
   1976                 }
   1977             };
   1978 
   1979             monitor.worked(1);
   1980 
   1981             return change;
   1982 
   1983         } finally {
   1984             monitor.done();
   1985         }
   1986 
   1987     }
   1988 
   1989     /**
   1990      * Given a file project path, returns its resource in the same project than the
   1991      * compilation unit. The resource may not exist.
   1992      */
   1993     private IResource getTargetXmlResource(String xmlFileWsPath) {
   1994         IResource resource = mProject.getFile(xmlFileWsPath);
   1995         return resource;
   1996     }
   1997 }
   1998