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