Automated import from //branches/master/...@141483,141483
This commit is contained in:
committed by
The Android Open Source Project
parent
106ca48f72
commit
8f87735e01
@@ -41,7 +41,9 @@ Require-Bundle: com.android.ide.eclipse.ddms,
|
||||
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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user