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