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