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