OSDN Git Service

ADT string refactoring: replace in all files.
authorRaphael Moll <ralf@android.com>
Sun, 28 Nov 2010 07:18:24 +0000 (23:18 -0800)
committerRaphael Moll <ralf@android.com>
Tue, 30 Nov 2010 20:14:08 +0000 (12:14 -0800)
When doing an extract string either from Java or XML:
- can scan/replace in all other Java files.
- can scan/replace in all other XML files.
- in Java, also replace in assignements.
- in XML, also replace existing string name if already defined.

Change-Id: Ifeef5fd444c2c18b9c071955b8e8567d6515ea95

eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/DisabledTextEditGroup.java [new file with mode: 0755]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceFolderType.java

diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/DisabledTextEditGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/DisabledTextEditGroup.java
new file mode 100755 (executable)
index 0000000..15f6c47
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.refactorings.extractstring;
+
+import org.eclipse.text.edits.TextEditGroup;
+
+/**
+ * A {@link TextEditGroup} that we want to enable or disable by default.
+ * This is used to propose a change that may not compile, so we'll create
+ * a change that is disabled.
+ * <p/>
+ * Disabling the change is done by the refactoring class when processing
+ * the text edit groups generated by the Java AST visitor.
+ */
+class EnabledTextEditGroup extends TextEditGroup {
+    private final boolean mEnabled;
+
+    public EnabledTextEditGroup(String name, boolean enabled) {
+        super(name);
+        mEnabled = enabled;
+    }
+
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+}
index cbcd581..8dab07e 100644 (file)
@@ -36,8 +36,10 @@ import org.eclipse.swt.events.ModifyEvent;
 import org.eclipse.swt.events.ModifyListener;
 import org.eclipse.swt.events.SelectionAdapter;
 import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Combo;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Group;
@@ -70,6 +72,10 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
     private ConfigurationSelector mConfigSelector;
     /** The combo to display the existing XML files or enter a new one. */
     private Combo mResFileCombo;
+    /** Checkbox asking whether to replace in all Java files. */
+    private Button mReplaceAllJava;
+    /** Checkbox asking whether to replace in all XML files with same name but other res config */
+    private Button mReplaceAllXml;
 
     /** Regex pattern to read a valid res XML file path. It checks that the are 2 folders and
      *  a leaf file name ending with .xml */
@@ -86,6 +92,20 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
 
     private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper();
 
+    private final OnConfigSelectorUpdated mOnConfigSelectorUpdated = new OnConfigSelectorUpdated();
+
+    private ModifyListener mValidateOnModify = new ModifyListener() {
+        public void modifyText(ModifyEvent e) {
+            validatePage();
+        }
+    };
+
+    private SelectionListener mValidateOnSelection = new SelectionAdapter() {
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            validatePage();
+        }
+    };
 
     public ExtractStringInputPage(IProject project) {
         super("ExtractStringInputPage");  //$NON-NLS-1$
@@ -97,17 +117,21 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
      * <p/>
      * Note that at that point the initial conditions have been checked in
      * {@link ExtractStringRefactoring}.
+     * <p/>
+     *
+     * Note: the special tag below defines this as the entry point for the WindowsDesigner Editor.
+     * @wbp.parser.entryPoint
      */
     public void createControl(Composite parent) {
         Composite content = new Composite(parent, SWT.NONE);
         GridLayout layout = new GridLayout();
-        layout.numColumns = 1;
         content.setLayout(layout);
 
         createStringGroup(content);
         createResFileGroup(content);
+        createOptionGroup(content);
 
-        validatePage();
+        initUi();
         setControl(content);
     }
 
@@ -123,10 +147,9 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
 
         Group group = new Group(content, SWT.NONE);
         group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        group.setText("New String");
         if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) {
             group.setText("String Replacement");
-        } else {
-            group.setText("New String");
         }
 
         GridLayout layout = new GridLayout();
@@ -152,19 +175,14 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
             }
         });
 
-
-        // TODO provide an option to replace all occurences of this string instead of
-        // just the one.
-
         // line : Textfield for new ID
 
         label = new Label(group, SWT.NONE);
+        label.setText("ID &R.string.");
         if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) {
             label.setText("&Replace by R.string.");
         } else if (ref.getMode() == ExtractStringRefactoring.Mode.SELECT_NEW_ID) {
             label.setText("New &R.string.");
-        } else {
-            label.setText("ID &R.string.");
         }
 
         mStringIdCombo = new Combo(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER | SWT.DROP_DOWN);
@@ -174,17 +192,8 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
 
         ref.setNewStringId(mStringIdCombo.getText().trim());
 
-        mStringIdCombo.addModifyListener(new ModifyListener() {
-            public void modifyText(ModifyEvent e) {
-                validatePage();
-            }
-        });
-        mStringIdCombo.addSelectionListener(new SelectionAdapter() {
-            @Override
-            public void widgetSelected(SelectionEvent e) {
-                validatePage();
-            }
-        });
+        mStringIdCombo.addModifyListener(mValidateOnModify);
+        mStringIdCombo.addSelectionListener(mValidateOnSelection);
     }
 
     /**
@@ -196,7 +205,9 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
     private void createResFileGroup(Composite content) {
 
         Group group = new Group(content, SWT.NONE);
-        group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+        gd.grabExcessVerticalSpace = true;
+        group.setLayoutData(gd);
         group.setText("XML resource to edit");
 
         GridLayout layout = new GridLayout();
@@ -210,13 +221,12 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
         label.setText("&Configuration:");
 
         mConfigSelector = new ConfigurationSelector(group, SelectorMode.DEFAULT);
-        GridData gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
+        gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
         gd.horizontalSpan = 2;
         gd.widthHint = ConfigurationSelector.WIDTH_HINT;
         gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
         mConfigSelector.setLayoutData(gd);
-        OnConfigSelectorUpdated onConfigSelectorUpdated = new OnConfigSelectorUpdated();
-        mConfigSelector.setOnChangeListener(onConfigSelectorUpdated);
+        mConfigSelector.setOnChangeListener(mOnConfigSelectorUpdated);
 
         // line: selection of the output file
 
@@ -226,15 +236,50 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
         mResFileCombo = new Combo(group, SWT.DROP_DOWN);
         mResFileCombo.select(0);
         mResFileCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
-        mResFileCombo.addModifyListener(onConfigSelectorUpdated);
+        mResFileCombo.addModifyListener(mOnConfigSelectorUpdated);
+    }
 
-        // set output file name to the last one used
+    /**
+     * Creates the bottom option groups with a few checkboxes.
+     *
+     * @param content A composite with a 1-column grid layout
+     */
+    private void createOptionGroup(Composite content) {
+        Group options = new Group(content, SWT.NONE);
+        options.setText("Options");
+        GridData gd_Options = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
+        gd_Options.widthHint = 77;
+        options.setLayoutData(gd_Options);
+        options.setLayout(new GridLayout(1, false));
+
+        mReplaceAllJava = new Button(options, SWT.CHECK);
+        mReplaceAllJava.setToolTipText("When checked, the exact same string literal will be replaced in all Java files.");
+        mReplaceAllJava.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        mReplaceAllJava.setText("Replace in all &Java files");
+        mReplaceAllJava.addSelectionListener(mValidateOnSelection);
+
+        mReplaceAllXml = new Button(options, SWT.CHECK);
+        mReplaceAllXml.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        mReplaceAllXml.setToolTipText("When checked, string literals will be replaced in other XML resource files having the same name but located in different resource configuration folders.");
+        mReplaceAllXml.setText("Replace in all &XML files for different configuration");
+        mReplaceAllXml.addSelectionListener(mValidateOnSelection);
+    }
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from WindowsDesigner Editor
+    //$hide>>$
 
+    /**
+     * Init UI just after it has been created the first time.
+     */
+    private void initUi() {
+        // set output file name to the last one used
         String projPath = mProject.getFullPath().toPortableString();
         String filePath = sLastResFilePath.get(projPath);
 
         mResFileCombo.setText(filePath != null ? filePath : DEFAULT_RES_FILE_PATH);
-        onConfigSelectorUpdated.run();
+        mOnConfigSelectorUpdated.run();
+        validatePage();
     }
 
     /**
@@ -278,6 +323,9 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
 
         ExtractStringRefactoring ref = getOurRefactoring();
 
+        ref.setReplaceAllJava(mReplaceAllJava.getSelection());
+        ref.setReplaceAllXml(mReplaceAllXml.isEnabled() && mReplaceAllXml.getSelection());
+
         // Analyze fatal errors.
 
         String text = mStringIdCombo.getText().trim();
@@ -372,7 +420,7 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
         }
     }
 
-    public class OnConfigSelectorUpdated implements Runnable, ModifyListener {
+    private class OnConfigSelectorUpdated implements Runnable, ModifyListener {
 
         /** Regex pattern to parse a valid res path: it reads (/res/folder-name/)+(filename). */
         private final Pattern mPathRegex = Pattern.compile(
@@ -422,9 +470,10 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
             mConfigSelector.getConfiguration(mTempConfig);
             StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
             sb.append(mTempConfig.getFolderName(ResourceFolderType.VALUES));
-            sb.append('/');
+            sb.append(AndroidConstants.WS_SEP);
 
             String newPath = sb.toString();
+
             if (newPath.equals(currPath) && newPath.equals(mLastFolderUsedInCombo)) {
                 // Path has not changed. No need to reload.
                 return;
@@ -546,4 +595,7 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
         }
     }
 
+    // End of hiding from SWT Designer
+    //$hide<<$
+
 }
index aa2b49d..b621a69 100644 (file)
@@ -31,6 +31,7 @@ import com.android.sdklib.xml.ManifestData;
 
 import org.eclipse.core.resources.IContainer;
 import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
 import org.eclipse.core.resources.IProject;
 import org.eclipse.core.resources.IResource;
 import org.eclipse.core.resources.ResourceAttributes;
@@ -43,6 +44,9 @@ import org.eclipse.core.runtime.Path;
 import org.eclipse.core.runtime.SubMonitor;
 import org.eclipse.jdt.core.IBuffer;
 import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
 import org.eclipse.jdt.core.JavaCore;
 import org.eclipse.jdt.core.JavaModelException;
 import org.eclipse.jdt.core.ToolFactory;
@@ -82,10 +86,14 @@ import org.w3c.dom.Node;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 
 /**
  * This refactoring extracts a string from a file and replaces it by an Android resource ID
@@ -176,8 +184,13 @@ public class ExtractStringRefactoring extends Refactoring {
     /** The XML string value. Might be different than the initial selected string. */
     private String mXmlStringValue;
     /** The path of the XML file that will define {@link #mXmlStringId}, selected by the user
-     *  in the wizard. */
+     *  in the wizard. This is relative to the project, e.g. "/res/values/string.xml" */
     private String mTargetXmlFileWsPath;
+    /** True if we should find & replace in all Java files. */
+    private boolean mReplaceAllJava;
+    /** True if we should find & replace in all XML files of the same name in other res configs
+     * (other than the main {@link #mTargetXmlFileWsPath}.) */
+    private boolean mReplaceAllXml;
 
     /** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and
      *  used by {@link #createChange(IProgressMonitor)}. */
@@ -185,15 +198,29 @@ public class ExtractStringRefactoring extends Refactoring {
 
     private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper();
 
-    private static final String KEY_MODE = "mode";              //$NON-NLS-1$
-    private static final String KEY_FILE = "file";              //$NON-NLS-1$
-    private static final String KEY_PROJECT = "proj";           //$NON-NLS-1$
-    private static final String KEY_SEL_START = "sel-start";    //$NON-NLS-1$
-    private static final String KEY_SEL_END = "sel-end";        //$NON-NLS-1$
-    private static final String KEY_TOK_ESC = "tok-esc";        //$NON-NLS-1$
-    private static final String KEY_XML_ATTR_NAME = "xml-attr-name";      //$NON-NLS-1$
+    private static final String KEY_MODE = "mode";                      //$NON-NLS-1$
+    private static final String KEY_FILE = "file";                      //$NON-NLS-1$
+    private static final String KEY_PROJECT = "proj";                   //$NON-NLS-1$
+    private static final String KEY_SEL_START = "sel-start";            //$NON-NLS-1$
+    private static final String KEY_SEL_END = "sel-end";                //$NON-NLS-1$
+    private static final String KEY_TOK_ESC = "tok-esc";                //$NON-NLS-1$
+    private static final String KEY_XML_ATTR_NAME = "xml-attr-name";    //$NON-NLS-1$
+    private static final String KEY_RPLC_ALL_JAVA = "rplc-all-java";    //$NON-NLS-1$
+    private static final String KEY_RPLC_ALL_XML  = "rplc-all-xml";     //$NON-NLS-1$
 
+    /**
+     * This constructor is solely used by {@link ExtractStringDescriptor},
+     * to replay a previous refactoring.
+     * <p/>
+     * To create a refactoring from code, please use one of the two other constructors.
+     *
+     * @param arguments A map previously created using {@link #createArgumentMap()}.
+     * @throws NullPointerException
+     */
     public ExtractStringRefactoring(Map<String, String> arguments) throws NullPointerException {
+
+        mReplaceAllJava = Boolean.parseBoolean(arguments.get(KEY_RPLC_ALL_JAVA));
+        mReplaceAllXml  = Boolean.parseBoolean(arguments.get(KEY_RPLC_ALL_XML));
         mMode = Mode.valueOf(arguments.get(KEY_MODE));
 
         IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT));
@@ -219,6 +246,8 @@ public class ExtractStringRefactoring extends Refactoring {
 
     private Map<String, String> createArgumentMap() {
         HashMap<String, String> args = new HashMap<String, String>();
+        args.put(KEY_RPLC_ALL_JAVA, Boolean.toString(mReplaceAllJava));
+        args.put(KEY_RPLC_ALL_XML,  Boolean.toString(mReplaceAllXml));
         args.put(KEY_MODE,      mMode.name());
         args.put(KEY_PROJECT,   mProject.getFullPath().toPortableString());
         if (mMode == Mode.EDIT_SOURCE) {
@@ -253,10 +282,13 @@ public class ExtractStringRefactoring extends Refactoring {
     /**
      * Constructor to use when the Extract String refactoring is called without
      * any source file. Its purpose is then to create a new XML string ID.
+     * <p/>
+     * For example this is currently invoked by the ResourceChooser when
+     * the user wants to create a new string rather than select an existing one.
      *
      * @param project The project where the target XML file to modify is located. Cannot be null.
-     * @param enforceNew If true the XML ID must be a new one. If false, an existing ID can be
-     *  used.
+     * @param enforceNew If true the XML ID must be a new one.
+     *                   If false, an existing ID can be used.
      */
     public ExtractStringRefactoring(IProject project, boolean enforceNew) {
         mMode = enforceNew ? Mode.SELECT_NEW_ID : Mode.SELECT_ID;
@@ -267,6 +299,36 @@ public class ExtractStringRefactoring extends Refactoring {
     }
 
     /**
+     * Sets the replacement string ID. Used by the wizard to set the user input.
+     */
+    public void setNewStringId(String newStringId) {
+        mXmlStringId = newStringId;
+    }
+
+    /**
+     * Sets the replacement string ID. Used by the wizard to set the user input.
+     */
+    public void setNewStringValue(String newStringValue) {
+        mXmlStringValue = newStringValue;
+    }
+
+    /**
+     * Sets the target file. This is a project path, e.g. "/res/values/strings.xml".
+     * Used by the wizard to set the user input.
+     */
+    public void setTargetFile(String targetXmlFileWsPath) {
+        mTargetXmlFileWsPath = targetXmlFileWsPath;
+    }
+
+    public void setReplaceAllJava(boolean replaceAllJava) {
+        mReplaceAllJava = replaceAllJava;
+    }
+
+    public void setReplaceAllXml(boolean replaceAllXml) {
+        mReplaceAllXml = replaceAllXml;
+    }
+
+    /**
      * @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
      */
     @Override
@@ -785,7 +847,7 @@ public class ExtractStringRefactoring extends Refactoring {
         RefactoringStatus status = new RefactoringStatus();
 
         try {
-            monitor.beginTask("Checking post-conditions...", 3);
+            monitor.beginTask("Checking post-conditions...", 5);
 
             if (mXmlStringId == null || mXmlStringId.length() <= 0) {
                 // this is not supposed to happen
@@ -820,10 +882,10 @@ public class ExtractStringRefactoring extends Refactoring {
             mChanges = new ArrayList<Change>();
 
 
-            // Prepare the change for the XML file.
-
-            if (mXmlHelper.valueOfStringId(mProject, mTargetXmlFileWsPath, mXmlStringId) == null) {
-                // We actually change it only if the ID doesn't exist yet
+            // Prepare the change to create/edit the String ID in the res/values XML file.
+            if (!mXmlStringValue.equals(
+                    mXmlHelper.valueOfStringId(mProject, mTargetXmlFileWsPath, mXmlStringId))) {
+                // We actually change it only if the ID doesn't exist yet or has a different value
                 Change change = createXmlChange((IFile) targetXml, mXmlStringId, mXmlStringValue,
                         status, SubMonitor.convert(monitor, 1));
                 if (change != null) {
@@ -853,6 +915,46 @@ public class ExtractStringRefactoring extends Refactoring {
                 }
             }
 
+            if (mReplaceAllJava) {
+                SubMonitor submon = SubMonitor.convert(monitor, 1);
+                for (ICompilationUnit unit : findAllJavaUnits()) {
+                    // Only process Java compilation units that exist, are not derived
+                    // and are not read-only.
+                    if (unit == null || !unit.exists()) {
+                        continue;
+                    }
+                    IResource resource = unit.getResource();
+                    if (resource == null || resource.isDerived()) {
+                        continue;
+                    }
+                    ResourceAttributes attrs = resource.getResourceAttributes();
+                    if (attrs != null && attrs.isReadOnly()) {
+                        continue;
+                    }
+
+                    List<Change> changes = computeJavaChanges(
+                            unit, mXmlStringId, mTokenString,
+                            status, SubMonitor.convert(submon, 1));
+                    if (changes != null) {
+                        mChanges.addAll(changes);
+                    }
+                }
+            }
+
+            if (mReplaceAllXml) {
+                SubMonitor submon = SubMonitor.convert(monitor, 1);
+                for (IFile xmlFile : findAllResXmlFiles()) {
+                    if (xmlFile != null) {
+                        List<Change> changes = computeXmlSourceChanges(xmlFile,
+                                mXmlStringId, mTokenString, mXmlAttributeName,
+                                status, SubMonitor.convert(submon, 1));
+                        if (changes != null) {
+                            mChanges.addAll(changes);
+                        }
+                    }
+                }
+            }
+
             monitor.worked(1);
         } finally {
             monitor.done();
@@ -861,6 +963,108 @@ public class ExtractStringRefactoring extends Refactoring {
         return status;
     }
 
+    // --- XML changes ---
+
+    /**
+     * Returns a foreach-compatible iterator over all XML files in the project's
+     * /res folder, excluding the target XML file (the one where we'll write/edit
+     * the string id).
+     */
+    private Iterable<IFile> findAllResXmlFiles() {
+        return new Iterable<IFile>() {
+            public Iterator<IFile> iterator() {
+                return new Iterator<IFile>() {
+                    final Queue<IFile> mFiles = new LinkedList<IFile>();
+                    final Queue<IResource> mFolders = new LinkedList<IResource>();
+                    IPath mFilterPath1 = null;
+                    IPath mFilterPath2 = null;
+                    {
+                        // We want to process the manifest
+                        IResource man = mProject.findMember("AndroidManifest.xml"); // TODO find a constant
+                        if (man.exists() && man instanceof IFile) {
+                            mFiles.add((IFile) man);
+                        }
+
+                        // Add all /res folders (technically we don't need to process /res/values
+                        // XML files that contain resources/string elements, but it's easier to
+                        // not filter them out.)
+                        IFolder f = mProject.getFolder(AndroidConstants.WS_RESOURCES);
+                        if (f.exists()) {
+                            try {
+                                mFolders.addAll(
+                                        Arrays.asList(f.members(IContainer.EXCLUDE_DERIVED)));
+                            } catch (CoreException e) {
+                                // pass
+                            }
+                        }
+
+                        // Filter out the XML file where we'll be writing the XML string id.
+                        IResource filterRes = mProject.findMember(mTargetXmlFileWsPath);
+                        if (filterRes != null) {
+                            mFilterPath1 = filterRes.getFullPath();
+                        }
+                        // Filter out the XML source file, if any (e.g. typically a layout)
+                        if (mFile != null) {
+                            mFilterPath2 = mFile.getFullPath();
+                        }
+                    }
+
+                    public boolean hasNext() {
+                        if (!mFiles.isEmpty()) {
+                            return true;
+                        }
+
+                        while (!mFolders.isEmpty()) {
+                            IResource res = mFolders.poll();
+                            if (res.exists() && res instanceof IFolder) {
+                                IFolder f = (IFolder) res;
+                                try {
+                                    getFileList(f);
+                                    if (!mFiles.isEmpty()) {
+                                        return true;
+                                    }
+                                } catch (CoreException e) {
+                                    // pass
+                                }
+                            }
+                        }
+                        return false;
+                    }
+
+                    private void getFileList(IFolder folder) throws CoreException {
+                        for (IResource res : folder.members(IContainer.EXCLUDE_DERIVED)) {
+                            // Only accept file resources which are not derived and actually exist
+                            if (res.exists() && !res.isDerived() && res instanceof IFile) {
+                                IFile file = (IFile) res;
+                                // Must have an XML extension
+                                if (AndroidConstants.EXT_XML.equals(file.getFileExtension())) {
+                                    IPath p = file.getFullPath();
+                                    // And not be either paths we want to filter out
+                                    if ((mFilterPath1 != null && mFilterPath1.equals(p)) ||
+                                            (mFilterPath2 != null && mFilterPath2.equals(p))) {
+                                        continue;
+                                    }
+                                    mFiles.add(file);
+                                }
+                            }
+                        }
+                    }
+
+                    public IFile next() {
+                        IFile file = mFiles.poll();
+                        hasNext();
+                        return file;
+                    }
+
+                    public void remove() {
+                        throw new UnsupportedOperationException(
+                            "This iterator does not support removal");  //$NON-NLS-1$
+                    }
+                };
+            }
+        };
+    }
+
     /**
      * Internal helper that actually prepares the {@link Change} that adds the given
      * ID to the given XML File.
@@ -881,7 +1085,7 @@ public class ExtractStringRefactoring extends Refactoring {
         TextFileChange xmlChange = new TextFileChange(getName(), targetXml);
         xmlChange.setTextType(AndroidConstants.EXT_XML);
 
-        String error = "";
+        String error = "";                  //$NON-NLS-1$
         TextEdit edit = null;
         TextEditGroup editGroup = null;
 
@@ -901,8 +1105,8 @@ public class ExtractStringRefactoring extends Refactoring {
 
         if (edit == null) {
             status.addFatalError(String.format("Failed to modify file %1$s%2$s",
-                    mTargetXmlFileWsPath,
-                    error == null ? "" : ": " + error)); //$NON-NLS-1$
+                    targetXml == null ? "" : targetXml.getFullPath(),   //$NON-NLS-1$
+                    error == null ? "" : ": " + error));                //$NON-NLS-1$
             return null;
         }
 
@@ -1288,7 +1492,8 @@ public class ExtractStringRefactoring extends Refactoring {
                                 // Remove " or ' quoting present in the attribute value
                                 text = unquoteAttrValue(text);
 
-                                if (xmlAttrName.equals(lastAttrName) && tokenString.equals(text)) {
+                                if (tokenString.equals(text) &&
+                                        (xmlAttrName == null || xmlAttrName.equals(lastAttrName))) {
 
                                     // Found an occurrence. Create a change for it.
                                     TextEdit edit = new ReplaceEdit(
@@ -1359,6 +1564,67 @@ public class ExtractStringRefactoring extends Refactoring {
         return '"' + attrValue + '"';
     }
 
+    // --- Java changes ---
+
+    /**
+     * Returns a foreach compatible iterator over all ICompilationUnit in the project.
+     */
+    private Iterable<ICompilationUnit> findAllJavaUnits() {
+        final IJavaProject javaProject = JavaCore.create(mProject);
+
+        return new Iterable<ICompilationUnit>() {
+            public Iterator<ICompilationUnit> iterator() {
+                return new Iterator<ICompilationUnit>() {
+                    final Queue<ICompilationUnit> mUnits = new LinkedList<ICompilationUnit>();
+                    final Queue<IPackageFragment> mFragments = new LinkedList<IPackageFragment>();
+                    {
+                        try {
+                            IPackageFragment[] tmpFrags = javaProject.getPackageFragments();
+                            if (tmpFrags != null && tmpFrags.length > 0) {
+                                mFragments.addAll(Arrays.asList(tmpFrags));
+                            }
+                        } catch (JavaModelException e) {
+                            // pass
+                        }
+                    }
+
+                    public boolean hasNext() {
+                        if (!mUnits.isEmpty()) {
+                            return true;
+                        }
+
+                        while (!mFragments.isEmpty()) {
+                            try {
+                                IPackageFragment fragment = mFragments.poll();
+                                if (fragment.getKind() == IPackageFragmentRoot.K_SOURCE) {
+                                    ICompilationUnit[] tmpUnits = fragment.getCompilationUnits();
+                                    if (tmpUnits != null && tmpUnits.length > 0) {
+                                        mUnits.addAll(Arrays.asList(tmpUnits));
+                                        return true;
+                                    }
+                                }
+                            } catch (JavaModelException e) {
+                                // pass
+                            }
+                        }
+                        return false;
+                    }
+
+                    public ICompilationUnit next() {
+                        ICompilationUnit unit = mUnits.poll();
+                        hasNext();
+                        return unit;
+                    }
+
+                    public void remove() {
+                        throw new UnsupportedOperationException(
+                                "This iterator does not support removal");  //$NON-NLS-1$
+                    }
+                };
+            }
+        };
+    }
+
     /**
      * Computes the changes to be made to Java file(s) and returns a list of {@link Change}.
      */
@@ -1395,23 +1661,6 @@ public class ExtractStringRefactoring extends Refactoring {
             return null;
         }
 
-        // TODO in a future version we might want to collect various Java files that
-        // need to be updated in the same project and process them all together.
-        // To do that we need to use an ASTRequestor and parser.createASTs, kind of
-        // like this:
-        //
-        // ASTRequestor requestor = new ASTRequestor() {
-        //    @Override
-        //    public void acceptAST(ICompilationUnit sourceUnit, CompilationUnit astNode) {
-        //        super.acceptAST(sourceUnit, astNode);
-        //        // TODO process astNode
-        //    }
-        // };
-        // ...
-        // parser.createASTs(compilationUnits, bindingKeys, requestor, monitor)
-        //
-        // and then add multiple TextFileChange to the changes arraylist.
-
         // Right now the changes array will contain one TextFileChange at most.
         ArrayList<Change> changes = new ArrayList<Change>();
 
@@ -1471,7 +1720,11 @@ public class ExtractStringRefactoring extends Refactoring {
                 // Create TextEditChangeGroups which let the user turn changes on or off
                 // individually. This must be done after the change.setEdit() call above.
                 for (TextEditGroup editGroup : astEditGroups) {
-                    change.addTextEditChangeGroup(new TextEditChangeGroup(change, editGroup));
+                    TextEditChangeGroup group = new TextEditChangeGroup(change, editGroup);
+                    if (editGroup instanceof EnabledTextEditGroup) {
+                        group.setEnabled(((EnabledTextEditGroup) editGroup).isEnabled());
+                    }
+                    change.addTextEditChangeGroup(group);
                 }
 
                 changes.add(change);
@@ -1494,6 +1747,8 @@ public class ExtractStringRefactoring extends Refactoring {
         return null;
     }
 
+    // ----
+
     /**
      * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the
      * work and creates a descriptor that can be used to replay that refactoring later.
@@ -1548,27 +1803,4 @@ public class ExtractStringRefactoring extends Refactoring {
         IResource resource = mProject.getFile(xmlFileWsPath);
         return resource;
     }
-
-    /**
-     * Sets the replacement string ID. Used by the wizard to set the user input.
-     */
-    public void setNewStringId(String newStringId) {
-        mXmlStringId = newStringId;
-    }
-
-    /**
-     * Sets the replacement string ID. Used by the wizard to set the user input.
-     */
-    public void setNewStringValue(String newStringValue) {
-        mXmlStringValue = newStringValue;
-    }
-
-    /**
-     * Sets the target file. This is a project path, e.g. "/res/values/strings.xml".
-     * Used by the wizard to set the user input.
-     */
-    public void setTargetFile(String targetXmlFileWsPath) {
-        mTargetXmlFileWsPath = targetXmlFileWsPath;
-    }
-
 }
index dd0f9f4..1a0521b 100755 (executable)
@@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.internal.refactorings.extractstring;
 import org.eclipse.jdt.core.dom.AST;\r
 import org.eclipse.jdt.core.dom.ASTNode;\r
 import org.eclipse.jdt.core.dom.ASTVisitor;\r
+import org.eclipse.jdt.core.dom.Assignment;\r
 import org.eclipse.jdt.core.dom.ClassInstanceCreation;\r
 import org.eclipse.jdt.core.dom.Expression;\r
 import org.eclipse.jdt.core.dom.IMethodBinding;\r
@@ -88,15 +89,16 @@ class ReplaceStringsVisitor extends ASTVisitor {
             // or if we should generate a Context.getString() call.\r
             boolean useGetResource = false;\r
             useGetResource = examineVariableDeclaration(node) ||\r
-                                examineMethodInvocation(node);\r
+                                examineMethodInvocation(node) ||\r
+                                examineAssignment(node);\r
 \r
             Name qualifierName = mAst.newName(mRQualifier + ".string");     //$NON-NLS-1$\r
             SimpleName idName = mAst.newSimpleName(mXmlId);\r
             ASTNode newNode = mAst.newQualifiedName(qualifierName, idName);\r
+            boolean disabledChange = false;\r
             String title = "Replace string by ID";\r
 \r
             if (useGetResource) {\r
-\r
                 Expression context = methodHasContextArgument(node);\r
                 if (context == null && !isClassDerivedFromContext(node)) {\r
                     // if we don't have a class that derives from Context and\r
@@ -106,8 +108,10 @@ class ReplaceStringsVisitor extends ASTVisitor {
 \r
                     if (context == null) {\r
                         // If not, let's  write Context.getString(), which is technically\r
-                        // invalid but makes it a good clue on how to fix it.\r
+                        // invalid but makes it a good clue on how to fix it. Since these\r
+                        // will not compile, we create a disabled change by default.\r
                         context = mAst.newSimpleName("Context");            //$NON-NLS-1$\r
+                        disabledChange = true;\r
                     }\r
                 }\r
 \r
@@ -120,7 +124,7 @@ class ReplaceStringsVisitor extends ASTVisitor {
                 title = "Replace string by Context.getString(R.string...)";\r
             }\r
 \r
-            TextEditGroup editGroup = new TextEditGroup(title);\r
+            TextEditGroup editGroup = new EnabledTextEditGroup(title, !disabledChange);\r
             mEditGroups.add(editGroup);\r
             mRewriter.replace(node, newNode, editGroup);\r
         }\r
@@ -128,8 +132,8 @@ class ReplaceStringsVisitor extends ASTVisitor {
     }\r
 \r
     /**\r
-     * Examines if the StringLiteral is part of of an assignment to a string,\r
-     * e.g. String foo = id.\r
+     * Examines if the StringLiteral is part of an assignment corresponding to the\r
+     * a string variable declaration, e.g. String foo = id.\r
      *\r
      * The parent fragment is of syntax "var = expr" or "var[] = expr".\r
      * We want the type of the variable, which is either held by a\r
@@ -161,6 +165,24 @@ class ReplaceStringsVisitor extends ASTVisitor {
     }\r
 \r
     /**\r
+     * Examines if the StringLiteral is part of a assignment to a variable that\r
+     * is a string. We need to lookup the variable to find its type, either in the\r
+     * enclosing method or class type.\r
+     */\r
+    private boolean examineAssignment(StringLiteral node) {\r
+\r
+        Assignment assignment = findParentClass(node, Assignment.class);\r
+        if (assignment != null) {\r
+            Expression left = assignment.getLeftHandSide();\r
+\r
+            ITypeBinding typeBinding = left.resolveTypeBinding();\r
+            return isJavaString(typeBinding);\r
+        }\r
+\r
+        return false;\r
+    }\r
+\r
+    /**\r
      * If the expression is part of a method invocation (aka a function call) or a\r
      * class instance creation (aka a "new SomeClass" constructor call), we try to\r
      * find the type of the argument being used. If it is a String (most likely), we\r
@@ -412,9 +434,11 @@ class ReplaceStringsVisitor extends ASTVisitor {
      */\r
     @SuppressWarnings("unchecked")\r
     private <T extends ASTNode> T findParentClass(ASTNode node, Class<T> clazz) {\r
-        for (node = node.getParent(); node != null; node = node.getParent()) {\r
-            if (node.getClass().equals(clazz)) {\r
-                return (T) node;\r
+        if (node != null) {\r
+            for (node = node.getParent(); node != null; node = node.getParent()) {\r
+                if (node.getClass().equals(clazz)) {\r
+                    return (T) node;\r
+                }\r
             }\r
         }\r
         return null;\r
index 0a56ff5..735a23c 100644 (file)
@@ -38,10 +38,13 @@ public enum ResourceFolderType {
         mName = name;
     }
 
+    /**
+     * Returns the folder name for this resource folder type.
+     */
     public String getName() {
         return mName;
     }
-    
+
     /**
      * Returns the enum by name.
      * @param name The enum string value.
@@ -55,7 +58,7 @@ public enum ResourceFolderType {
         }
         return null;
     }
-    
+
     /**
      * Returns the {@link ResourceFolderType} from the folder name
      * @param folderName The name of the folder. This must be a valid folder name in the format