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 */ ExtractStringRefactoring(Map<String, String> arguments)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 createArgumentMap()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 */ ExtractStringRefactoring(IFile file, IEditorPart editor, ITextSelection selection)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 */ ExtractStringRefactoring(IProject project, boolean enforceNew)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 */ setNewStringId(String newStringId)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 */ setNewStringValue(String newStringValue)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 */ setTargetFile(String targetXmlFileWsPath)322 public void setTargetFile(String targetXmlFileWsPath) { 323 mTargetXmlFileWsPath = targetXmlFileWsPath; 324 } 325 setReplaceAllJava(boolean replaceAllJava)326 public void setReplaceAllJava(boolean replaceAllJava) { 327 mReplaceAllJava = replaceAllJava; 328 } 329 setReplaceAllXml(boolean replaceAllXml)330 public void setReplaceAllXml(boolean replaceAllXml) { 331 mReplaceAllXml = replaceAllXml; 332 } 333 334 /** 335 * @see org.eclipse.ltk.core.refactoring.Refactoring#getName() 336 */ 337 @Override getName()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 getMode()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 */ getTokenString()356 public String getTokenString() { 357 return mTokenString; 358 } 359 360 /** Returns the XML string ID selected by the user in the wizard. */ getXmlStringId()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 checkInitialConditions(IProgressMonitor monitor)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 */ findSelectionInJavaUnit(ICompilationUnit unit, RefactoringStatus status, IProgressMonitor monitor)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 */ findSelectionInXmlFile(IFile file, RefactoringStatus status, IProgressMonitor monitor)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 */ findSelectionInRegion(IStructuredDocumentRegion region, int selStart)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 */ unquoteAttrValue(String attrValue)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 */ validateSelectedAttribute(AndroidXmlEditor editor, Node node, String attrName, RefactoringStatus status)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 */ checkSourceFile(IFile file, RefactoringStatus status, IProgressMonitor monitor)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 checkFinalConditions(IProgressMonitor monitor)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 */ findAllResXmlFiles()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 ' or " 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