Automated import from //branches/master/...@141483,141483
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
index 55c18bb..c0dfcef 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
@@ -41,7 +41,9 @@
  org.eclipse.wst.xml.core,
  org.eclipse.wst.xml.ui,
  org.eclipse.jdt.junit,
- org.eclipse.jdt.junit.runtime
+ org.eclipse.jdt.junit.runtime,
+ org.eclipse.ltk.core.refactoring,
+ org.eclipse.ltk.ui.refactoring
 Eclipse-LazyStart: true
 Export-Package: com.android.ide.eclipse.adt,
  com.android.ide.eclipse.adt.build;x-friends:="com.android.ide.eclipse.tests",
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
index 2ab6425..2bf633d 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
@@ -470,7 +470,7 @@
          point="org.eclipse.ui.actionSets">
       <actionSet
             description="Android Wizards"
-            id="adt.actionSet1"
+            id="adt.actionSet.wizards"
             label="Android Wizards"
             visible="true">
          <action
@@ -481,12 +481,6 @@
                style="push"
                toolbarPath="android_project"
                tooltip="Opens a wizard to help create a new Android XML file">
-               <enablement>
-                   <objectState
-                        name="projectNature"
-                        value="com.android.ide.eclipse.adt.AndroidNature">
-                   </objectState>
-               </enablement>
          </action>
          <action
                class="com.android.ide.eclipse.adt.wizards.actions.NewProjectAction"
@@ -498,6 +492,21 @@
                tooltip="Opens a wizard to help create a new Android project">
          </action>
       </actionSet>
+      <actionSet
+            description="Refactorings for Android"
+            id="adt.actionSet.refactorings"
+            label="Android Refactorings"
+            visible="true">
+         <action
+               class="com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringAction"
+               definitionId="com.android.ide.eclipse.adt.refactoring.extract.string"
+               id="com.android.ide.eclipse.adt.actions.ExtractString"
+               label="Extract Android String"
+               menubarPath="org.eclipse.jdt.ui.refactoring.menu/codingGroup"
+               style="push"
+               tooltip="Extracts a string into Android resource string">
+         </action>
+      </actionSet>
    </extension>
    <extension
          point="org.eclipse.debug.core.launchDelegates">
@@ -565,4 +574,25 @@
          </configurationType>
       </shortcut>
    </extension>
+   <extension
+         point="org.eclipse.ui.commands">
+      <category
+            description="Refactorings for Android Projects"
+            id="com.android.ide.eclipse.adt.refactoring.category"
+            name="Android Refactorings">
+      </category>
+      <command
+            categoryId="com.android.ide.eclipse.adt.refactoring.category"
+            description="Extract Strings into Android String Resources"
+            id="com.android.ide.eclipse.adt.refactoring.extract.string"
+            name="Extract Android String">
+      </command>
+   </extension>
+   <extension
+         point="org.eclipse.ltk.core.refactoring.refactoringContributions">
+      <contribution
+            class="com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringContribution"
+            id="com.android.ide.eclipse.adt.refactoring.extract.string">
+      </contribution>
+   </extension>
 </plugin>
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java
new file mode 100644
index 0000000..5287015
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2009 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.refactorings.extractstring;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.ITypeRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.IWorkbenchWindowActionDelegate;
+import org.eclipse.ui.PlatformUI;
+
+/*
+ * Quick Reference Link:
+ * http://www.eclipse.org/articles/article.php?file=Article-Unleashing-the-Power-of-Refactoring/index.html
+ * and
+ * http://www.ibm.com/developerworks/opensource/library/os-ecjdt/
+ */
+
+/**
+ * Action executed when the "Extract String" menu item is invoked.
+ * <p/>
+ * The intent of the action is to start a refactoring that extracts a source string and
+ * replaces it by an Android string resource ID. 
+ * <p/>
+ * Workflow:
+ * <ul>
+ * <li> The action is currently located in the Refactoring menu in the main menu.
+ * <li> TODO: extend the popup refactoring menu in a Java or Android XML file.
+ * <li> The action is only enabled if the selection is 1 character or more. That is at least part
+ *     of the string must be selected, it's not enough to just move the insertion point. This is
+ *     a limitation due to {@link #selectionChanged(IAction, ISelection)} not being called when
+ *     the insertion point is merely moved. TODO: address this limitation.
+ * <ul> The action gets the current {@link ISelection}. It also knows the current
+ *     {@link IWorkbenchWindow}. However for the refactoring we are also interested in having the
+ *     actual resource file. By looking at the Active Window > Active Page > Active Editor we
+ *     can get the {@link IEditorInput} and find the {@link ICompilationUnit} (aka Java file)
+ *     that is being edited.
+ * <ul> TODO: change this to find the {@link IFile} being manipulated. The {@link ICompilationUnit}
+ *     can be inferred using {@link JavaCore#createCompilationUnitFrom(IFile)}. This will allow
+ *     us to be able to work with a selection from an Android XML file later.
+ * <li> The action creates a new {@link ExtractStringRefactoring} and make it run on in a new
+ *     {@link ExtractStringWizard}.
+ * <ul>
+ */
+public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
+
+    /** Keep track of the current workbench window. */
+    private IWorkbenchWindow mWindow;
+    private ITextSelection mSelection;
+    private ICompilationUnit mUnit;
+
+    /**
+     * Keep track of the current workbench window.
+     */
+    public void init(IWorkbenchWindow window) {
+        mWindow = window;
+    }
+
+    public void dispose() {
+        // Nothing to do
+    }
+
+    /**
+     * Examine the selection to determine if the action should be enabled or not.
+     * <p/>
+     * Keep a link to the relevant selection structure (i.e. a part of the Java AST).
+     */
+    public void selectionChanged(IAction action, ISelection selection) {
+
+        // Note, two kinds of selections are returned here:
+        // ITextSelection on a Java source window
+        // IStructuredSelection in the outline or navigator
+        // This simply deals with the refactoring based on a non-empty selection.
+        // At that point, just enable the action and later decide if it's valid when it actually
+        // runs since we don't have access to the AST yet.
+
+        mSelection = null;
+        mUnit = null;
+        
+        if (selection instanceof ITextSelection) {
+            mSelection = (ITextSelection) selection;
+            if (mSelection.getLength() > 0) {
+                mUnit = getCompilationUnit();
+            }
+
+            // Keep for debugging purposes
+            //System.out.println(String.format("-- Selection: %d + %d = %s",
+            //        mSelection.getOffset(),
+            //        mSelection.getLength(),
+            //        mSelection.getText()));
+        }
+
+        action.setEnabled(mSelection != null && mUnit != null);
+    }
+
+    /**
+     * Create a new instance of our refactoring and a wizard to configure it.
+     */
+    public void run(IAction action) {
+        if (mSelection != null && mUnit != null) {
+            ExtractStringRefactoring ref = new ExtractStringRefactoring(mUnit, mSelection);
+            RefactoringWizard wizard = new ExtractStringWizard(ref, "Extract Android String");
+            RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+            try {
+                op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+            } catch (InterruptedException e) {
+                // Interrupted. Pass.
+            }
+        }
+    }
+
+    /**
+     * Returns the active {@link ICompilationUnit} or null.
+     */
+    private ICompilationUnit getCompilationUnit() {
+        IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+        if (wwin != null) {
+            IWorkbenchPage page = wwin.getActivePage();
+            if (page != null) {
+                IEditorPart editor = page.getActiveEditor();
+                if (editor != null) {
+                    IEditorInput input = editor.getEditorInput();
+                    if (input != null) {
+                        ITypeRoot typeRoot = JavaUI.getEditorInputTypeRoot(input);
+                        // The type root can be either a .class or a .java (aka compilation unit).
+                        // We want the compilation unit kind.
+                        if (typeRoot instanceof ICompilationUnit) {
+                            return (ICompilationUnit) typeRoot;
+                        }
+                    }
+                }
+            }
+        }
+        
+        return null;
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java
new file mode 100644
index 0000000..465e1a3
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2009 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.refactorings.extractstring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+/**
+ * @see ExtractStringDescriptor
+ */
+public class ExtractStringContribution extends RefactoringContribution {
+    
+    /* (non-Javadoc)
+     * @see org.eclipse.ltk.core.refactoring.RefactoringContribution#createDescriptor(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.Map, int)
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public RefactoringDescriptor createDescriptor(
+            String id,
+            String project,
+            String description,
+            String comment,
+            Map arguments,
+            int flags)
+                throws IllegalArgumentException {
+        return new ExtractStringDescriptor(project, description, comment, arguments);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+        if (descriptor instanceof ExtractStringDescriptor) {
+            return ((ExtractStringDescriptor) descriptor).getArguments();
+        }
+        return super.retrieveArgumentMap(descriptor);
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java
new file mode 100644
index 0000000..6e999e9
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2009 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.refactorings.extractstring;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+
+import java.util.Map;
+
+/**
+ * A descriptor that allows an {@link ExtractStringRefactoring} to be created from
+ * a previous instance of itself.
+ */
+public class ExtractStringDescriptor extends RefactoringDescriptor {
+
+    public static final String ID =
+        "com.android.ide.eclipse.adt.refactoring.extract.string";  //$NON-NLS-1$
+    
+    private final Map<String, String> mArguments;
+
+    public ExtractStringDescriptor(String project, String description, String comment,
+            Map<String, String> arguments) {
+        super(ID, project, description, comment,
+                RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE //flags
+        );
+        mArguments = arguments;
+    }
+    
+    public Map<String, String> getArguments() {
+        return mArguments;
+    }
+
+    /**
+     * Creates a new refactoring instance for this refactoring descriptor based on
+     * an argument map. The argument map is created by the refactoring itself in
+     * {@link ExtractStringRefactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)}
+     * <p/>
+     * This is apparently used to replay a refactoring.
+     * 
+     * {@inheritDoc}
+     * 
+     * @throws CoreException
+     */
+    @Override
+    public Refactoring createRefactoring(RefactoringStatus status) throws CoreException {
+        try {
+            ExtractStringRefactoring ref = new ExtractStringRefactoring(mArguments);
+            return ref;
+        } catch (NullPointerException e) {
+            status.addFatalError("Failed to recreate ExtractStringRefactoring from descriptor");
+            return null;
+        }
+    }
+
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java
new file mode 100644
index 0000000..cb449f0
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2009 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.refactorings.extractstring;
+
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * @see ExtractStringRefactoring
+ */
+class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage {
+
+    public ExtractStringInputPage() {
+        super("ExtractStringInputPage");  //$NON-NLS-1$
+    }
+
+    private Label mStringLabel;
+    private Text mNewIdTextField;
+    private Label mFileLabel;
+
+    /**
+     * Create the UI for the refactoring wizard.
+     * <p/>
+     * Note that at that point the initial conditions have been checked in
+     * {@link ExtractStringRefactoring}.
+     */
+    public void createControl(Composite parent) {
+        
+        final ExtractStringRefactoring ref = getOurRefactoring();
+        
+        Composite content = new Composite(parent, SWT.NONE);
+
+        GridLayout layout = new GridLayout();
+        layout.numColumns = 2;
+        content.setLayout(layout);
+
+        // line 1: String found in selection
+        
+        Label label = new Label(content, SWT.NONE);
+        label.setText("String:");
+
+        String selectedString = ref.getTokenString();
+        
+        mStringLabel = new Label(content, SWT.NONE);
+        mStringLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mStringLabel.setText(selectedString != null ? selectedString : "");
+        
+        // TODO provide an option to replace all occurences of this string instead of
+        // just the one.
+
+        // line 2 : Textfield for new ID
+        
+        label = new Label(content, SWT.NONE);
+        label.setText("Replace by R.string.");
+
+        mNewIdTextField = new Text(content, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+        mNewIdTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mNewIdTextField.setText(guessId(selectedString));
+
+        ref.setReplacementStringId(mNewIdTextField.getText().trim());
+
+        mNewIdTextField.addModifyListener(new ModifyListener() {
+            public void modifyText(ModifyEvent e) {
+                if (validatePage(ref)) {
+                    ref.setReplacementStringId(mNewIdTextField.getText().trim());
+                }
+            }
+        });
+
+        // line 3: selection of the output file
+        // TODO add a file field/chooser combo to let the user select the file to edit.
+
+        label = new Label(content, SWT.NONE);
+        label.setText("Resource file:");
+
+        mFileLabel = new Label(content, SWT.NONE);
+        mFileLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mFileLabel.setText("/res/values/strings.xml");
+        ref.setTargetFile(mFileLabel.getText());
+
+        // line 4: selection of the res config
+        // TODO add the Configuration Selector to decide with strings.xml to change
+
+        label = new Label(content, SWT.NONE);
+        label.setText("Configuration:");
+
+        label = new Label(content, SWT.NONE);
+        label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        label.setText("default");
+        
+        validatePage(ref);
+        setControl(content);
+    }
+
+    private String guessId(String text) {
+        // make lower case
+        text = text.toLowerCase();
+        
+        // everything not alphanumeric becomes an underscore
+        text = text.replaceAll("[^a-zA-Z0-9]+", "_");  //$NON-NLS-1$ //$NON-NLS-2$
+
+        // the id must be a proper Java identifier, so it can't start with a number
+        if (text.length() > 0 && !Character.isJavaIdentifierStart(text.charAt(0))) {
+            text = "_" + text;  //$NON-NLS-1$
+        }
+        return text;
+    }
+
+    private ExtractStringRefactoring getOurRefactoring() {
+        return (ExtractStringRefactoring) getRefactoring();
+    }
+
+    private boolean validatePage(ExtractStringRefactoring ref) {
+        String text = mNewIdTextField.getText().trim();
+        boolean success = true;
+
+        // Analyze fatal errors.
+        
+        if (text == null || text.length() < 1) {
+            setErrorMessage("Please provide a resource ID to replace with.");
+            success = false;
+        } else {
+            for (int i = 0; i < text.length(); i++) {
+                char c = text.charAt(i);
+                boolean ok = i == 0 ?
+                        Character.isJavaIdentifierStart(c) :
+                        Character.isJavaIdentifierPart(c);
+                if (!ok) {
+                    setErrorMessage(String.format(
+                            "The resource ID must be a valid Java identifier. The character %1$c at position %2$d is not acceptable.",
+                            c, i+1));
+                    success = false;
+                    break;
+                }
+            }
+        }
+
+        // Analyze info & warnings.
+        
+        if (success) {
+            if (ref.isResIdDuplicate(mFileLabel.getText(), text)) {
+                setErrorMessage(null);
+                setMessage(
+                    String.format("Warning: There's already a string item called '%1$s' in %2$s.",
+                        text, mFileLabel.getText()));
+            } else {
+                setMessage(null);
+                setErrorMessage(null);
+            }
+        }
+
+        setPageComplete(success);
+        return success;
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java
new file mode 100644
index 0000000..715503c
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java
@@ -0,0 +1,890 @@
+/*
+ * Copyright (C) 2009 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.refactorings.extractstring;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+import com.android.ide.eclipse.common.project.AndroidXPathFactory;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.ResourceAttributes;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+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.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.ToolFactory;
+import org.eclipse.jdt.core.compiler.IScanner;
+import org.eclipse.jdt.core.compiler.ITerminalSymbols;
+import org.eclipse.jdt.core.compiler.InvalidInputException;
+import org.eclipse.jdt.core.dom.AST;
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.ASTParser;
+import org.eclipse.jdt.core.dom.ASTVisitor;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.dom.Name;
+import org.eclipse.jdt.core.dom.QualifiedName;
+import org.eclipse.jdt.core.dom.SimpleName;
+import org.eclipse.jdt.core.dom.StringLiteral;
+import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
+import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.text.edits.TextEditGroup;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * This refactoring extracts a string from a file and replaces it by an Android resource ID
+ * such as R.string.foo.
+ * <p/>
+ * There are a number of scenarios, which are not all supported yet. The workflow works as
+ * such:
+ * <ul>
+ * <li> User selects a string in a Java (TODO: or XML file) and invokes
+ *      the {@link ExtractStringAction}.
+ * <li> The action finds the {@link ICompilationUnit} being edited as well as the current
+ *      {@link ITextSelection}. The action creates a new instance of this refactoring as
+ *      well as an {@link ExtractStringWizard} and runs the operation.
+ * <li> TODO: to support refactoring from an XML file, the action should give the {@link IFile}
+ *      and then here we would have to determine whether it's a suitable Android XML file or a
+ *      suitable Java file.
+ *      TODO: enumerate the exact valid contexts in Android XML files, e.g. attributes in layout
+ *      files or text elements (e.g. <string>foo</string>) for values, etc. 
+ * <li> Step 1 of the refactoring is to check the preliminary conditions. Right now we check
+ *      that the java source is not read-only and is in sync. We also try to find a string under
+ *      the selection. If this fails, the refactoring is aborted.
+ * <li> TODO: Find the string in an XML file based on selection.
+ * <li> On success, the wizard is shown, which let the user input the new ID to use.
+ * <li> The wizard sets the user input values into this refactoring instance, e.g. the new string
+ *      ID, the XML file to update, etc. The wizard does use the utility method
+ *      {@link #isResIdDuplicate(String, String)} to check whether the new ID is already defined
+ *      in the target XML file.
+ * <li> Once Preview or Finish is selected in the wizard, the
+ *      {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input
+ *      and compute the actual changes.
+ * <li> When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked.
+ * </ul>
+ * 
+ * The list of changes are:
+ * <ul>
+ * <li> If the target XML does not exist, create it with the new string ID.
+ * <li> If the target XML exists, find the <resources> node and add the new string ID right after.
+ *      If the node is <resources/>, it needs to be opened.
+ * <li> Create an AST rewriter to edit the source Java file and replace all occurences by the
+ *      new computed R.string.foo. Also need to rewrite imports to import R as needed.
+ *      If there's already a conflicting R included, we need to insert the FQCN instead.
+ * <li> TODO: If the source is an XML file, determine if we need to change an attribute or a
+ *      a text element.
+ * <li> TODO: Have a pref in the wizard: [x] Change other XML Files
+ * <li> TODO: Have a pref in the wizard: [x] Change other Java Files
+ * </ul>
+ */
+class ExtractStringRefactoring extends Refactoring {
+
+    /** The compilation unit, a.k.a. the Java file model. */
+    private final ICompilationUnit mUnit;
+    private final int mSelectionStart;
+    private final int mSelectionEnd;
+    /** The actual string selected, after UTF characters have been escaped, good for display. */
+    private String mTokenString;
+    /** Start position of the string token in the source buffer. */
+    private int mTokenStart;
+    /** End position of the string token in the source buffer. */
+    private int mTokenEnd;
+    private String mXmlStringId;
+    private String mTargetXmlFileWsPath;
+    private HashMap<String,HashSet<String>> mResIdCache;
+    private XPath mXPath;
+    private ArrayList<Change> mChanges;
+
+    public ExtractStringRefactoring(Map<String, String> arguments)
+            throws NullPointerException {
+        mUnit = (ICompilationUnit) JavaCore.create(arguments.get("CU"));    //$NON-NLS-1$
+        mSelectionStart = Integer.parseInt(arguments.get("sel-start"));     //$NON-NLS-1$
+        mSelectionEnd   = Integer.parseInt(arguments.get("sel-end"));       //$NON-NLS-1$
+        mTokenStart     = Integer.parseInt(arguments.get("tok-start"));     //$NON-NLS-1$
+        mTokenEnd       = Integer.parseInt(arguments.get("tok-end"));       //$NON-NLS-1$
+        mTokenString    = arguments.get("tok-esc");                         //$NON-NLS-1$
+    }
+    
+    private Map<String, String> createArgumentMap() {
+        HashMap<String, String> args = new HashMap<String, String>();
+        args.put("CU",        mUnit.getHandleIdentifier());                 //$NON-NLS-1$
+        args.put("sel-start", Integer.toString(mSelectionStart));           //$NON-NLS-1$
+        args.put("sel-end",   Integer.toString(mSelectionEnd));             //$NON-NLS-1$
+        args.put("tok-start", Integer.toString(mTokenStart));               //$NON-NLS-1$
+        args.put("tok-end",   Integer.toString(mTokenEnd));                 //$NON-NLS-1$
+        args.put("tok-esc",   mTokenString);                                //$NON-NLS-1$
+        return args;
+    }
+
+    public ExtractStringRefactoring(ICompilationUnit unit, ITextSelection selection) {
+        mUnit = unit;
+
+        mSelectionStart = selection.getOffset();
+        mSelectionEnd = mSelectionStart + selection.getLength();
+    }
+
+    /**
+     * @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
+     */
+    @Override
+    public String getName() {
+        return "Extract Android String";
+    }
+    
+    /**
+     * Gets the actual string selected, after UTF characters have been escaped,
+     * good for display.
+     */
+    public String getTokenString() {
+        return mTokenString;
+    }
+    
+    /**
+     * Step 1 of 3 of the refactoring:
+     * Checks that the current selection meets the initial condition before the ExtractString
+     * wizard is shown. The check is supposed to be lightweight and quick. Note that at that
+     * point the wizard has not been created yet.
+     * <p/>
+     * Here we scan the source buffer to find the token matching the selection.
+     * The check is successful is a Java string literal is selected, the source is in sync
+     * and is not read-only.
+     * <p/>
+     * This is also used to extract the string to be modified, so that we can display it in
+     * the refactoring wizard.
+     * 
+     * @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor)
+     * 
+     * @throws CoreException 
+     */
+    @Override
+    public RefactoringStatus checkInitialConditions(IProgressMonitor monitor)
+            throws CoreException, OperationCanceledException {
+
+        mTokenString = null;
+        mTokenStart = -1;
+        mTokenEnd = -1;
+
+        RefactoringStatus status = new RefactoringStatus();
+        
+        try {
+            monitor.beginTask("Checking preconditions...", 3);
+
+            if (!extraChecks(monitor, status)) {
+                return status;
+            }
+            
+            try {
+                IBuffer buffer = mUnit.getBuffer();
+
+                IScanner scanner = ToolFactory.createScanner(
+                        false, //tokenizeComments
+                        false, //tokenizeWhiteSpace
+                        false, //assertMode
+                        false  //recordLineSeparator
+                        );
+                scanner.setSource(buffer.getCharacters());
+                monitor.worked(1);
+
+                for(int token = scanner.getNextToken();
+                        token != ITerminalSymbols.TokenNameEOF;
+                        token = scanner.getNextToken()) {
+                    if (scanner.getCurrentTokenStartPosition() <= mSelectionStart &&
+                            scanner.getCurrentTokenEndPosition() >= mSelectionEnd) {
+                        // found the token, but only keep of the right type
+                        if (token == ITerminalSymbols.TokenNameStringLiteral) {
+                            mTokenString = new String(scanner.getCurrentTokenSource());
+                            mTokenStart = scanner.getCurrentTokenStartPosition();
+                            mTokenEnd = scanner.getCurrentTokenEndPosition();
+                        }
+                        break;
+                    } else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) {
+                        // scanner is past the selection, abort.
+                        break;
+                    }
+                }
+            } catch (JavaModelException e1) {
+                // Error in mUnit.getBuffer. Ignore.
+            } catch (InvalidInputException e2) {
+                // Error in scanner.getNextToken. Ignore.
+            } finally {
+                monitor.worked(1);
+            }
+
+            if (mTokenString != null) {
+                // As a literal string, the token should have surrounding quotes. Remove them.
+                int len = mTokenString.length();
+                if (len > 0 &&
+                        mTokenString.charAt(0) == '"' &&
+                        mTokenString.charAt(len - 1) == '"') {
+                    mTokenString = mTokenString.substring(1, len - 1);
+                }
+                // We need a non-empty string literal
+                if (mTokenString.length() == 0) {
+                    mTokenString = null;
+                }
+            }
+            
+            if (mTokenString == null) {
+                status.addFatalError("Please select a Java string literal.");
+            }
+            
+            monitor.worked(1);
+        } finally {
+            monitor.done();
+        }
+        
+        return status;
+    }
+
+    /**
+     * Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit()
+     * Might not be useful.
+     * 
+     * @return False if caller should abort, true if caller should continue.
+     */
+    private boolean extraChecks(IProgressMonitor monitor, RefactoringStatus status) {
+        // 
+        IResource res = mUnit.getPrimary().getResource();
+        if (res == null || res.getType() != IResource.FILE) {
+            status.addFatalError("Cannot access resource; only regular files can be used.");
+            return false;
+        }
+        monitor.worked(1);
+
+        // check whether the source file is in sync
+        if (!res.isSynchronized(IResource.DEPTH_ZERO)) {
+            status.addFatalError("The file is not synchronized. Please save it first.");
+            return false;
+        }
+        monitor.worked(1);
+        
+        // make sure we can write to it.
+        ResourceAttributes resAttr = res.getResourceAttributes();
+        if (mUnit.isReadOnly() || resAttr == null || resAttr.isReadOnly()) {
+            status.addFatalError("The file is read-only, please make it writeable first.");
+            return false;
+        }
+        monitor.worked(1);
+        
+        return true;
+    }
+
+    /**
+     * Step 2 of 3 of the refactoring:
+     * Check the conditions once the user filled values in the refactoring wizard,
+     * then prepare the changes to be applied.
+     * <p/>
+     * In this case, most of the sanity checks are done by the wizard so essentially this
+     * should only be called if the wizard positively validated the user input.
+     * 
+     * Here we do check that the target resource XML file either does not exists or
+     * is not read-only.
+     * 
+     * @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor)
+     * 
+     * @throws CoreException 
+     */
+    @Override
+    public RefactoringStatus checkFinalConditions(IProgressMonitor monitor)
+            throws CoreException, OperationCanceledException {
+        RefactoringStatus status = new RefactoringStatus();
+
+        try {
+            monitor.beginTask("Checking post-conditions...", 3);
+
+            if (mXmlStringId == null || mXmlStringId.length() <= 0) {
+                // this is not supposed to happen
+                status.addFatalError("Missing replacement string ID");
+            } else if (mTargetXmlFileWsPath == null || mTargetXmlFileWsPath.length() <= 0) {
+                // this is not supposed to happen
+                status.addFatalError("Missing target xml file path");
+            }
+            monitor.worked(1);
+            
+            // Either that resource must not exist or it must be a writeable file.
+            IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath);
+            if (targetXml != null) {
+                if (targetXml.getType() != IResource.FILE) {
+                    status.addFatalError(
+                            String.format("XML file '%1$s' is not a file.", mTargetXmlFileWsPath));
+                } else {
+                    ResourceAttributes attr = targetXml.getResourceAttributes();
+                    if (attr != null && attr.isReadOnly()) {
+                        status.addFatalError(
+                                String.format("XML file '%1$s' is read-only.",
+                                        mTargetXmlFileWsPath));
+                    }
+                }
+            }
+            monitor.worked(1);
+            
+            if (status.hasError()) {
+                return status;
+            }
+            
+            mChanges = new ArrayList<Change>();
+            
+            
+            // Prepare the change for the XML file.
+            
+            if (!isResIdDuplicate(mTargetXmlFileWsPath, mXmlStringId)) {
+                // We actually change it only if the ID doesn't exist yet
+                TextFileChange xmlChange = new TextFileChange(getName(), (IFile) targetXml);
+                xmlChange.setTextType("xml");   //$NON-NLS-1$
+                TextEdit edit = createXmlEdit((IFile) targetXml, mXmlStringId, mTokenString);
+                if (edit == null) {
+                    status.addFatalError(String.format("Failed to modify file %1$s",
+                            mTargetXmlFileWsPath));
+                }
+                xmlChange.setEdit(edit);
+                mChanges.add(xmlChange);
+            }
+            monitor.worked(1);
+    
+            if (status.hasError()) {
+                return status;
+            }
+            
+            // Prepare the change to the Java compilation unit
+            List<Change> changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString, status,
+                    SubMonitor.convert(monitor, 1));
+            if (changes != null) {
+                mChanges.addAll(changes);
+            }
+            
+            monitor.worked(1);
+        } finally {
+            monitor.done();
+        }
+        
+        return status;
+    }
+
+    /**
+     * Internal helper that actually prepares the {@link TextEdit} that adds the given
+     * ID to the given XML File.
+     * <p/>
+     * This does not actually modify the file.
+     *  
+     * @param xmlFile The file resource to modify.
+     * @param replacementStringId The new ID to insert.
+     * @param oldString The old string, which will be the value in the XML string.
+     * @return A new {@link TextEdit} that describes how to change the file.
+     */
+    private TextEdit createXmlEdit(IFile xmlFile, String replacementStringId, String oldString) {
+
+        if (!xmlFile.exists()) {
+            // The XML file does not exist. Simply create it.
+            StringBuilder content = new StringBuilder();
+            content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
+            content.append("<resources>\n");                                //$NON-NLS-1$
+            content.append("    <string name=\"").                          //$NON-NLS-1$
+                        append(replacementStringId).
+                        append("\">").                                      //$NON-NLS-1$
+                        append(oldString).
+                        append("</string>\n");                              //$NON-NLS-1$
+            content.append("<resources>\n");                                //$NON-NLS-1$
+
+            return new InsertEdit(0, content.toString());
+        }
+
+        // The file exist. Attempt to parse it as a valid XML document.
+        try {
+            int[] indices = new int[2];
+            if (findXmlOpeningTagPos(xmlFile.getContents(), "resources", indices)) {  //$NON-NLS-1$
+                // Indices[1] indicates whether we found > or />. It can only be 1 or 2.
+                // Indices[0] is the position of the first character of either > or />.
+                //
+                // Note: we don't even try to adapt our formatting to the existing structure (we
+                // could by capturing whatever whitespace is after the closing bracket and
+                // applying it here before our tag, unless we were dealing with an empty
+                // resource tag.)
+                
+                int offset = indices[0];
+                int len = indices[1];
+                StringBuilder content = new StringBuilder();
+                content.append(">\n");                                      //$NON-NLS-1$
+                content.append("    <string name=\"").                      //$NON-NLS-1$
+                            append(replacementStringId).
+                            append("\">").                                  //$NON-NLS-1$
+                            append(oldString).
+                            append("</string>");                            //$NON-NLS-1$
+                if (len == 2) {
+                    content.append("\n</resources>");                       //$NON-NLS-1$
+                }
+
+                return new ReplaceEdit(offset, len, content.toString());
+            }
+            
+        } catch (CoreException e) {
+            // Failed to read file. Ignore. Will return null below.
+        }
+        
+        return null;
+    }
+
+    /**
+     * Parse an XML input stream, looking for an opening tag.
+     * <p/>
+     * If found, returns the character offet in the buffer of the closing bracket of that
+     * tag, e.g. the position of > in "<resources>". The first character is at offset 0.
+     * <p/>
+     * The implementation here relies on a simple character-based parser. No DOM nor SAX
+     * parsing is used, due to the simplified nature of the task: we just want the first
+     * opening tag, which in our case should be the document root. We deal however with
+     * with the tag being commented out, so comments are skipped. We assume the XML doc
+     * is sane, e.g. we don't expect the tag to appear in the middle of a string. But
+     * again since in fact we want the root element, that's unlikely to happen.
+     * <p/>
+     * We need to deal with the case where the element is written as <resources/>, in
+     * which case the caller will want to replace /> by ">...</...>". To do that we return
+     * two values: the first offset of the closing tag (e.g. / or >) and the length, which
+     * can only be 1 or 2. If it's 2, the caller have to deal with /> instead of just >.
+     * 
+     * @param contents An existing buffer to parse.
+     * @param tag The tag to look for.
+     * @param indices The return values: [0] is the offset of the closing bracket and [1] is
+     *          the length which can be only 1 for > and 2 for />
+     * @return True if we found the tag, in which case <code>indices</code> can be used.
+     */
+    private boolean findXmlOpeningTagPos(InputStream contents, String tag, int[] indices) {
+
+        BufferedReader br = new BufferedReader(new InputStreamReader(contents));
+        StringBuilder sb = new StringBuilder(); // scratch area
+
+        tag = "<" + tag;
+        int tagLen = tag.length();
+        int maxLen = tagLen < 3 ? 3 : tagLen;
+        
+        try {
+            int offset = 0;
+            int i = 0;
+            char searching = '<'; // we want opening tags
+            boolean capture = false;
+            boolean inComment = false;
+            boolean inTag = false;
+            while ((i = br.read()) != -1) {
+                char c = (char) i;
+                if (c == searching) {
+                    capture = true;
+                }
+                if (capture) {
+                    sb.append(c);
+                    int len = sb.length();
+                    if (inComment && c == '>') {
+                        // is the comment being closed?
+                        if (len >= 3 && sb.substring(len-3).equals("-->")) {    //$NON-NLS-1$
+                            // yes, comment is closing, stop capturing
+                            capture = false;
+                            inComment = false;
+                            sb.setLength(0);
+                        }
+                    } else if (inTag && c == '>') {
+                        // we're capturing in our tag, waiting for the closing >, we just got it
+                        // so we're totally done here. Simply detect whether it's /> or >.
+                        indices[0] = offset;
+                        indices[1] = 1;
+                        if (sb.charAt(len - 2) == '/') {
+                            indices[0]--;
+                            indices[1]++;
+                        }
+                        return true;
+                        
+                    } else if (!inComment && !inTag) {
+                        // not a comment and not our tag yet, so we're capturing because a
+                        // tag is being opened but we don't know which one yet.
+                        
+                        // look for either the opening or a comment or
+                        // the opening of our tag.
+                        if (len == 3 && sb.equals("<--")) {                     //$NON-NLS-1$
+                            inComment = true;
+                        } else if (len == tagLen && sb.toString().equals(tag)) {
+                            inTag = true;
+                        }
+
+                        // if we're not interested in this tag yet, deal with when to stop
+                        // capturing: the opening tag ends with either any kind of whitespace
+                        // or with a > or maybe there's a PI that starts with <?
+                        if (!inComment && !inTag) {
+                            if (c == '>' || c == '?' || c == ' ' || c == '\n' || c == '\r') {
+                                // stop capturing
+                                capture = false;
+                                sb.setLength(0);
+                            }
+                        }
+                    }
+
+                    if (capture && len > maxLen) {
+                        // in any case we don't need to capture more than the size of our tag
+                        // or the comment opening tag
+                        sb.deleteCharAt(0);
+                    }
+                }
+                offset++;
+            }
+        } catch (IOException e) {
+            // Ignore.
+        } finally {
+            try {
+                br.close();
+            } catch (IOException e) {
+                // oh come on...
+            }
+        }
+        
+        return false;
+    }
+
+    private List<Change> computeJavaChanges(ICompilationUnit unit,
+            String xmlStringId,
+            String tokenString,
+            RefactoringStatus status,
+            SubMonitor subMonitor) {
+
+        // Get the Android package name from the Android Manifest. We need it to create
+        // the FQCN of the R class.
+        String packageName = null;
+        String error = null;
+        IProject proj = unit.getJavaProject().getProject();
+        IResource manifestFile = proj.findMember(AndroidConstants.FN_ANDROID_MANIFEST);
+        if (manifestFile == null || manifestFile.getType() != IResource.FILE) {
+            error = "File not found";
+        } else {
+            try {
+                AndroidManifestParser manifest = AndroidManifestParser.parseForData(
+                        (IFile) manifestFile);
+                if (manifest == null) {
+                    error = "Invalid content";
+                } else {
+                    packageName = manifest.getPackage();
+                    if (packageName == null) {
+                        error = "Missing package definition";
+                    }
+                }
+            } catch (CoreException e) {
+                error = e.getLocalizedMessage();
+            }
+        }
+        
+        if (error != null) {
+            status.addFatalError(
+                    String.format("Failed to parse file %1$s: %2$s.",
+                            manifestFile.getFullPath(), error));
+            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>();
+
+        // This is the unit that will be modified.
+        TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource());
+        change.setTextType("java"); //$NON-NLS-1$
+
+        // Create an AST for this compilation unit
+        ASTParser parser = ASTParser.newParser(AST.JLS3);
+        parser.setProject(unit.getJavaProject());
+        parser.setSource(unit);
+        parser.setResolveBindings(true);
+        ASTNode node = parser.createAST(subMonitor.newChild(1));
+
+        // The ASTNode must be a CompilationUnit, by design
+        if (!(node instanceof CompilationUnit)) {
+            status.addFatalError(String.format("Internal error: ASTNode class %s",  //$NON-NLS-1$
+                    node.getClass()));
+            return null;
+        }
+
+        // ImportRewrite will allow us to add the new type to the imports and will resolve
+        // what the Java source must reference, e.g. the FQCN or just the simple name.
+        ImportRewrite ir = ImportRewrite.create((CompilationUnit) node, true);
+        String Rqualifier = packageName + ".R"; //$NON-NLS-1$
+        Rqualifier = ir.addImport(Rqualifier);
+
+        // Rewrite the AST itself via an ASTVisitor
+        AST ast = node.getAST();
+        ASTRewrite ar = ASTRewrite.create(ast);
+        ReplaceStringsVisitor visitor = new ReplaceStringsVisitor(ast, ar,
+                tokenString, Rqualifier, xmlStringId);
+        node.accept(visitor);
+        
+        // Finally prepare the change set
+        try {
+            MultiTextEdit edit = new MultiTextEdit();
+
+            // Create the edit to change the imports, only if anything changed
+            TextEdit subEdit = ir.rewriteImports(subMonitor.newChild(1));
+            if (subEdit.hasChildren()) {
+                edit.addChild(subEdit);
+            }
+
+            // Create the edit to change the Java source, only if anything changed
+            subEdit = ar.rewriteAST();
+            if (subEdit.hasChildren()) {
+                edit.addChild(subEdit);
+            }
+
+            // Only create a change set if any edit was collected
+            if (edit.hasChildren()) {
+                change.setEdit(edit);
+                changes.add(change);
+            }
+            
+            // TODO to modify another Java source, loop back to the creation of the
+            // TextFileChange and accumulate in changes. Right now only one source is
+            // modified.
+            
+            if (changes.size() > 0) {
+                return changes;
+            }
+            
+        } catch (CoreException e) {
+            // ImportRewrite.rewriteImports failed.
+            status.addFatalError(e.getMessage());
+        }
+        return null;
+    }
+
+    public class ReplaceStringsVisitor extends ASTVisitor {
+
+        private final AST mAst;
+        private final ASTRewrite mRewriter;
+        private final String mOldString;
+        private final String mRQualifier;
+        private final String mXmlId;
+
+        public ReplaceStringsVisitor(AST ast,
+                ASTRewrite astRewrite,
+                String oldString,
+                String rQualifier,
+                String xmlId) {
+            mAst = ast;
+            mRewriter = astRewrite;
+            mOldString = oldString;
+            mRQualifier = rQualifier;
+            mXmlId = xmlId;
+        }
+
+        @Override
+        public boolean visit(StringLiteral node) {
+            if (node.getLiteralValue().equals(mOldString)) {
+                
+                Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$
+                SimpleName idName = mAst.newSimpleName(mXmlId);
+                QualifiedName newNode = mAst.newQualifiedName(qualifierName, idName);
+                
+                TextEditGroup editGroup = new TextEditGroup(getName());
+                mRewriter.replace(node, newNode, editGroup);
+            }
+            return super.visit(node);
+        }
+    }
+
+    /**
+     * 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. 
+     * 
+     * @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)
+     * 
+     * @throws CoreException 
+     */
+    @Override
+    public Change createChange(IProgressMonitor monitor)
+            throws CoreException, OperationCanceledException {
+
+        try {
+            monitor.beginTask("Applying changes...", 1);
+            
+            CompositeChange change = new CompositeChange(
+                    getName(),
+                    mChanges.toArray(new Change[mChanges.size()])) {
+                @Override
+                public ChangeDescriptor getDescriptor() {
+
+                    String comment = String.format(
+                            "Extracts string '%1$s' into R.string.%2$s",
+                            mTokenString,
+                            mXmlStringId);
+                    
+                    ExtractStringDescriptor desc = new ExtractStringDescriptor(
+                            mUnit.getJavaProject().getElementName(), //project
+                            comment, //description
+                            comment, //comment
+                            createArgumentMap());
+                    
+                    return new RefactoringChangeDescriptor(desc);
+                }
+            };
+            
+            monitor.worked(1);
+            
+            return change;
+            
+        } finally {
+            monitor.done();
+        }
+        
+    }
+
+    /**
+     * Utility method used by the wizard to check whether the given string ID is already
+     * defined in the XML file which path is given.
+     * 
+     * @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml".
+     *          The given file may or may not exist.
+     * @param stringId The string ID to find.
+     * @return True if such a string ID is already defined.
+     */
+    public boolean isResIdDuplicate(String xmlFileWsPath, String stringId) {
+        // This is going to be called many times on the same file.
+        // Build a cache of the existing IDs for a given file.
+        if (mResIdCache == null) {
+            mResIdCache = new HashMap<String, HashSet<String>>();
+        }
+        HashSet<String> cache = mResIdCache.get(xmlFileWsPath);
+        if (cache == null) {
+            cache = getResIdsForFile(xmlFileWsPath);
+            mResIdCache.put(xmlFileWsPath, cache);
+        }
+        
+        return cache.contains(stringId);
+    }
+
+    /**
+     * Extract all the defined string IDs from a given file using XPath.
+     * 
+     * @param xmlFileWsPath The project path of the file to parse. It may not exist.
+     * @return The set of all string IDs defined in the file. The returned set is always non
+     *   null. It is empty if the file does not exist.
+     */
+    private HashSet<String> getResIdsForFile(String xmlFileWsPath) {
+        HashSet<String> ids = new HashSet<String>();
+        
+        if (mXPath == null) {
+            mXPath = AndroidXPathFactory.newXPath();
+        }
+
+        // Access the project that contains the resource that contains the compilation unit
+        IResource resource = getTargetXmlResource(xmlFileWsPath);
+        
+        if (resource != null && resource.exists() && resource.getType() == IResource.FILE) {
+            InputSource source;
+            try {
+                source = new InputSource(((IFile) resource).getContents());
+
+                // We want all the IDs in an XML structure like this:
+                // <resources>
+                //    <string name="ID">something</string>
+                // </resources>
+                
+                String xpathExpr = "/resources/string/@name";   //$NON-NLS-1$
+                
+                Object result = mXPath.evaluate(xpathExpr, source, XPathConstants.NODESET);
+                if (result instanceof NodeList) {
+                    NodeList list = (NodeList) result;
+                    for (int n = list.getLength() - 1; n >= 0; n--) {
+                        String id = list.item(n).getNodeValue();
+                        ids.add(id);
+                    }
+                }
+                
+            } catch (CoreException e1) {
+                // IFile.getContents failed. Ignore.
+            } catch (XPathExpressionException e) {
+                // mXPath.evaluate failed. Ignore.
+            }
+        }
+        
+        return ids;
+    }
+
+    /**
+     * Given a file project path, returns its resource in the same project than the
+     * compilation unit. The resource may not exist.
+     */
+    private IResource getTargetXmlResource(String xmlFileWsPath) {
+        IProject proj = mUnit.getPrimary().getResource().getProject();
+        Path path = new Path(xmlFileWsPath);
+        IResource resource = proj.findMember(path);
+        return resource;
+    }
+
+    /**
+     * Sets the replacement string ID. Used by the wizard to set the user input.
+     */
+    public void setReplacementStringId(String replacementStringId) {
+        mXmlStringId = replacementStringId;
+    }
+
+    /**
+     * 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;
+    }
+
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java
new file mode 100644
index 0000000..2083a6e
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2009 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.refactorings.extractstring;
+
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+
+/**
+ * A wizard for ExtractString based on a simple dialog with one page.
+ * 
+ * @see ExtractStringInputPage
+ * @see ExtractStringRefactoring
+ */
+class ExtractStringWizard extends RefactoringWizard {
+
+    /**
+     * Create a wizard for ExtractString based on a simple dialog with one page.
+     */
+    public ExtractStringWizard(ExtractStringRefactoring ref, String title) {
+        super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
+        setDefaultPageTitle(title);
+    }
+
+    @Override
+    protected void addUserInputPages() {
+        addPage(new ExtractStringInputPage());
+    }
+
+}