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