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