FileDocCategorySizeDatePackage
ExtractStringRefactoring.javaAPI DocAndroid 1.5 API41645Wed May 06 22:41:10 BST 2009com.android.ide.eclipse.adt.refactorings.extractstring

ExtractStringRefactoring

public class ExtractStringRefactoring extends org.eclipse.ltk.core.refactoring.Refactoring
This refactoring extracts a string from a file and replaces it by an Android resource ID such as R.string.foo.

There are a number of scenarios, which are not all supported yet. The workflow works as such:

  • User selects a string in a Java (TODO: or XML file) and invokes the {@link ExtractStringAction}.
  • 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.
  • 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. foo) for values, etc.
  • 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.
  • TODO: Find the string in an XML file based on selection.
  • On success, the wizard is shown, which let the user input the new ID to use.
  • 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 XmlStringFileHelper#isResIdDuplicate(IProject, String, String)} to check whether the new ID is already defined in the target XML file.
  • 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.
  • When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked.
The list of changes are:
  • If the target XML does not exist, create it with the new string ID.
  • If the target XML exists, find the node and add the new string ID right after. If the node is , it needs to be opened.
  • 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.
  • TODO: If the source is an XML file, determine if we need to change an attribute or a a text element.
  • TODO: Have a pref in the wizard: [x] Change other XML Files
  • TODO: Have a pref in the wizard: [x] Change other Java Files

Fields Summary
private final Mode
mMode
The {@link Mode} of operation of the refactoring.
private final org.eclipse.core.resources.IFile
mFile
The file model being manipulated. Value is null when not on {@link Mode#EDIT_SOURCE} mode.
private final org.eclipse.core.resources.IProject
mProject
The project that contains {@link #mFile} and that contains the target XML file to modify.
private final int
mSelectionStart
The start of the selection in {@link #mFile}. Value is -1 when not on {@link Mode#EDIT_SOURCE} mode.
private final int
mSelectionEnd
The end of the selection in {@link #mFile}. Value is -1 when not on {@link Mode#EDIT_SOURCE} mode.
private org.eclipse.jdt.core.ICompilationUnit
mUnit
The compilation unit, only defined if {@link #mFile} points to a usable Java source file.
private String
mTokenString
The actual string selected, after UTF characters have been escaped, good for display. Value is null when not on {@link Mode#EDIT_SOURCE} mode.
private String
mXmlStringId
The XML string ID selected by the user in the wizard.
private String
mXmlStringValue
The XML string value. Might be different than the initial selected string.
private String
mTargetXmlFileWsPath
The path of the XML file that will define {@link #mXmlStringId}, selected by the user in the wizard.
private ArrayList
mChanges
The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and used by {@link #createChange(IProgressMonitor)}.
private XmlStringFileHelper
mXmlHelper
private static final String
KEY_MODE
private static final String
KEY_FILE
private static final String
KEY_PROJECT
private static final String
KEY_SEL_START
private static final String
KEY_SEL_END
private static final String
KEY_TOK_ESC
Constructors Summary
public ExtractStringRefactoring(Map arguments)

        //$NON-NLS-1$

       
              
        mMode = Mode.valueOf(arguments.get(KEY_MODE));

        IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT));
        mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
        
        if (mMode == Mode.EDIT_SOURCE) {
            path = Path.fromPortableString(arguments.get(KEY_FILE));
            mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);

            mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START));
            mSelectionEnd   = Integer.parseInt(arguments.get(KEY_SEL_END));
            mTokenString    = arguments.get(KEY_TOK_ESC);
        } else {
            mFile = null;
            mSelectionStart = mSelectionEnd = -1;
            mTokenString = null;
        }
    
public ExtractStringRefactoring(org.eclipse.core.resources.IFile file, org.eclipse.jface.text.ITextSelection selection)
Constructor to use when the Extract String refactoring is called on an *existing* source file. Its purpose is then to get the selected string of the source and propose to change it by an XML id. The XML id may be a new one or an existing one.

param
file The source file to process. Cannot be null. File must exist in workspace.
param
selection The selection in the source file. Cannot be null or empty.

        mMode = Mode.EDIT_SOURCE;
        mFile = file;
        mProject = file.getProject();
        mSelectionStart = selection.getOffset();
        mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1);
    
public ExtractStringRefactoring(org.eclipse.core.resources.IProject project, boolean enforceNew)
Constructor to use when the Extract String refactoring is called without any source file. Its purpose is then to create a new XML string ID.

param
project The project where the target XML file to modify is located. Cannot be null.
param
enforceNew If true the XML ID must be a new one. If false, an existing ID can be used.

        mMode = enforceNew ? Mode.SELECT_NEW_ID : Mode.SELECT_ID;
        mFile = null;
        mProject = project;
        mSelectionStart = mSelectionEnd = -1;
    
Methods Summary
public org.eclipse.ltk.core.refactoring.RefactoringStatuscheckFinalConditions(org.eclipse.core.runtime.IProgressMonitor monitor)
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.

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

        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 (!mXmlHelper.isResIdDuplicate(mProject, mTargetXmlFileWsPath, mXmlStringId)) {
                // We actually change it only if the ID doesn't exist yet
                Change change = createXmlChange((IFile) targetXml, mXmlStringId, mXmlStringValue,
                        status, SubMonitor.convert(monitor, 1));
                if (change != null) {
                    mChanges.add(change);
                }
            }

            if (status.hasError()) {
                return status;
            }

            if (mMode == Mode.EDIT_SOURCE) {
                // 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;
    
public org.eclipse.ltk.core.refactoring.RefactoringStatuscheckInitialConditions(org.eclipse.core.runtime.IProgressMonitor monitor)
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.

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.

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


        mUnit = null;
        mTokenString = null;

        RefactoringStatus status = new RefactoringStatus();
        
        try {
            monitor.beginTask("Checking preconditions...", 5);

            if (mMode != Mode.EDIT_SOURCE) {
                monitor.worked(5);
                return status;
            }
            
            if (!checkSourceFile(mFile, status, monitor)) {
                return status;
            }

            // Try to get a compilation unit from this file. If it fails, mUnit is null.
            try {
                mUnit = JavaCore.createCompilationUnitFrom(mFile);

                // Make sure the unit is not read-only, e.g. it's not a class file or inside a Jar
                if (mUnit.isReadOnly()) {
                    status.addFatalError("The file is read-only, please make it writeable first.");
                    return status;
                }
                
                // This is a Java file. Check if it contains the selection we want.
                if (!findSelectionInJavaUnit(mUnit, status, monitor)) {
                    return status;
                }
                
            } catch (Exception e) {
                // That was not a Java file. Ignore.
            }
            
            if (mUnit == null) {
                // Check this an XML file and get the selection and its context.
                // TODO
                status.addFatalError("Selection must be inside a Java source file.");
            }
        } finally {
            monitor.done();
        }
        
        return status;
    
private booleancheckSourceFile(org.eclipse.core.resources.IFile file, org.eclipse.ltk.core.refactoring.RefactoringStatus status, org.eclipse.core.runtime.IProgressMonitor monitor)
Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit() Might not be useful. On success, advance the monitor by 2.

return
False if caller should abort, true if caller should continue.

        // check whether the source file is in sync
        if (!file.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 = file.getResourceAttributes();
        if (resAttr == null || resAttr.isReadOnly()) {
            status.addFatalError("The file is read-only, please make it writeable first.");
            return false;
        }
        monitor.worked(1);
        
        return true;
    
private java.util.ListcomputeJavaChanges(org.eclipse.jdt.core.ICompilationUnit unit, java.lang.String xmlStringId, java.lang.String tokenString, org.eclipse.ltk.core.refactoring.RefactoringStatus status, org.eclipse.core.runtime.SubMonitor subMonitor)
Computes the changes to be made to Java file(s) and returns a list of {@link Change}.


        // 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;
        IResource manifestFile = mProject.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 importRewrite = ImportRewrite.create((CompilationUnit) node, true);
        String Rqualifier = packageName + ".R"; //$NON-NLS-1$
        Rqualifier = importRewrite.addImport(Rqualifier);

        // Rewrite the AST itself via an ASTVisitor
        AST ast = node.getAST();
        ASTRewrite astRewrite = ASTRewrite.create(ast);
        ArrayList<TextEditGroup> astEditGroups = new ArrayList<TextEditGroup>();
        ReplaceStringsVisitor visitor = new ReplaceStringsVisitor(
                ast, astRewrite, astEditGroups,
                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 = importRewrite.rewriteImports(subMonitor.newChild(1));
            if (subEdit.hasChildren()) {
                edit.addChild(subEdit);
            }

            // Create the edit to change the Java source, only if anything changed
            subEdit = astRewrite.rewriteAST();
            if (subEdit.hasChildren()) {
                edit.addChild(subEdit);
            }

            // Only create a change set if any edit was collected
            if (edit.hasChildren()) {
                change.setEdit(edit);
                
                // Create TextEditChangeGroups which let the user turn changes on or off
                // individually. This must be done after the change.setEdit() call above.
                for (TextEditGroup editGroup : astEditGroups) {
                    change.addTextEditChangeGroup(new TextEditChangeGroup(change, editGroup));
                }
                
                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;
            }

            subMonitor.worked(1);

        } catch (CoreException e) {
            // ImportRewrite.rewriteImports failed.
            status.addFatalError(e.getMessage());
        }
        return null;
    
private java.util.MapcreateArgumentMap()

        HashMap<String, String> args = new HashMap<String, String>();
        args.put(KEY_MODE,      mMode.name());
        args.put(KEY_PROJECT,   mProject.getFullPath().toPortableString());
        if (mMode == Mode.EDIT_SOURCE) {
            args.put(KEY_FILE,      mFile.getFullPath().toPortableString());
            args.put(KEY_SEL_START, Integer.toString(mSelectionStart));
            args.put(KEY_SEL_END,   Integer.toString(mSelectionEnd));
            args.put(KEY_TOK_ESC,   mTokenString);
        }
        return args;
    
public org.eclipse.ltk.core.refactoring.ChangecreateChange(org.eclipse.core.runtime.IProgressMonitor monitor)
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


        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(
                            mProject.getName(), //project
                            comment, //description
                            comment, //comment
                            createArgumentMap());
                    
                    return new RefactoringChangeDescriptor(desc);
                }
            };
            
            monitor.worked(1);
            
            return change;
            
        } finally {
            monitor.done();
        }
        
    
private org.eclipse.ltk.core.refactoring.ChangecreateXmlChange(org.eclipse.core.resources.IFile targetXml, java.lang.String xmlStringId, java.lang.String tokenString, org.eclipse.ltk.core.refactoring.RefactoringStatus status, org.eclipse.core.runtime.SubMonitor subMonitor)
Internal helper that actually prepares the {@link Change} that adds the given ID to the given XML File.

This does not actually modify the file.

param
targetXml The file resource to modify.
param
xmlStringId The new ID to insert.
param
tokenString The old string, which will be the value in the XML string.
return
A new {@link TextEdit} that describes how to change the file.


        TextFileChange xmlChange = new TextFileChange(getName(), targetXml);
        xmlChange.setTextType("xml");   //$NON-NLS-1$
        
        TextEdit edit = null;
        TextEditGroup editGroup = null;

        if (!targetXml.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(xmlStringId).
                        append("\">").                                      //$NON-NLS-1$
                        append(tokenString).
                        append("</string>\n");                              //$NON-NLS-1$
            content.append("<resources>\n");                                //$NON-NLS-1$

            edit = new InsertEdit(0, content.toString());
            editGroup = new TextEditGroup("Create <string> in new XML file", edit);
        } else {
            // The file exist. Attempt to parse it as a valid XML document.
            try {
                int[] indices = new int[2];
                
                // TODO case where we replace the value of an existing XML String ID
                
                if (findXmlOpeningTagPos(targetXml.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(xmlStringId).
                                append("\">").                                  //$NON-NLS-1$
                                append(tokenString).
                                append("</string>");                            //$NON-NLS-1$
                    if (len == 2) {
                        content.append("\n</resources>");                       //$NON-NLS-1$
                    }

                    edit = new ReplaceEdit(offset, len, content.toString());
                    editGroup = new TextEditGroup("Insert <string> in XML file", edit);
                }
            } catch (CoreException e) {
                // Failed to read file. Ignore. Will return null below.
            }
        }

        if (edit == null) {
            status.addFatalError(String.format("Failed to modify file %1$s",
                    mTargetXmlFileWsPath));
            return null;
        }

        xmlChange.setEdit(edit);
        // The TextEditChangeGroup let the user toggle this change on and off later.
        xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup));

        subMonitor.worked(1);
        return xmlChange;
    
private booleanfindSelectionInJavaUnit(org.eclipse.jdt.core.ICompilationUnit unit, org.eclipse.ltk.core.refactoring.RefactoringStatus status, org.eclipse.core.runtime.IProgressMonitor monitor)
Try to find the selected Java element in the compilation unit. If selection matches a string literal, capture it, otherwise add a fatal error to the status. On success, advance the monitor by 3.

        try {
            IBuffer buffer = unit.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());
                    }
                    break;
                } else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) {
                    // scanner is past the selection, abort.
                    break;
                }
            }
        } catch (JavaModelException e1) {
            // Error in unit.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);
        return status.isOK();
    
private booleanfindXmlOpeningTagPos(java.io.InputStream contents, java.lang.String tag, int[] indices)
Parse an XML input stream, looking for an opening tag.

If found, returns the character offest in the buffer of the closing bracket of that tag, e.g. the position of > in "". The first character is at offset 0.

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.

We need to deal with the case where the element is written as , 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 indices can be used.


        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;
    
public com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringRefactoring$ModegetMode()

        return mMode;
    
public java.lang.StringgetName()

see
org.eclipse.ltk.core.refactoring.Refactoring#getName()

        if (mMode == Mode.SELECT_ID) {
            return "Create or USe Android String";
        } else if (mMode == Mode.SELECT_NEW_ID) {
            return "Create New Android String";
        }

        return "Extract Android String";
    
private org.eclipse.core.resources.IResourcegetTargetXmlResource(java.lang.String xmlFileWsPath)
Given a file project path, returns its resource in the same project than the compilation unit. The resource may not exist.

        IResource resource = mProject.getFile(xmlFileWsPath);
        return resource;
    
public java.lang.StringgetTokenString()
Gets the actual string selected, after UTF characters have been escaped, good for display.

        return mTokenString;
    
public java.lang.StringgetXmlStringId()

        return mXmlStringId;
    
public voidsetNewStringId(java.lang.String newStringId)
Sets the replacement string ID. Used by the wizard to set the user input.

        mXmlStringId = newStringId;
    
public voidsetNewStringValue(java.lang.String newStringValue)
Sets the replacement string ID. Used by the wizard to set the user input.

        mXmlStringValue = newStringValue;
    
public voidsetTargetFile(java.lang.String targetXmlFileWsPath)
Sets the target file. This is a project path, e.g. "/res/values/strings.xml". Used by the wizard to set the user input.

        mTargetXmlFileWsPath = targetXmlFileWsPath;