1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.eclipse.org/org/documents/epl-v10.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ide.eclipse.adt.internal.refactorings.extractstring;
18
19import org.eclipse.jdt.core.dom.AST;
20import org.eclipse.jdt.core.dom.ASTNode;
21import org.eclipse.jdt.core.dom.ASTVisitor;
22import org.eclipse.jdt.core.dom.Assignment;
23import org.eclipse.jdt.core.dom.ClassInstanceCreation;
24import org.eclipse.jdt.core.dom.Expression;
25import org.eclipse.jdt.core.dom.IMethodBinding;
26import org.eclipse.jdt.core.dom.ITypeBinding;
27import org.eclipse.jdt.core.dom.IVariableBinding;
28import org.eclipse.jdt.core.dom.MethodDeclaration;
29import org.eclipse.jdt.core.dom.MethodInvocation;
30import org.eclipse.jdt.core.dom.Modifier;
31import org.eclipse.jdt.core.dom.Name;
32import org.eclipse.jdt.core.dom.SimpleName;
33import org.eclipse.jdt.core.dom.SimpleType;
34import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
35import org.eclipse.jdt.core.dom.StringLiteral;
36import org.eclipse.jdt.core.dom.Type;
37import org.eclipse.jdt.core.dom.TypeDeclaration;
38import org.eclipse.jdt.core.dom.VariableDeclarationExpression;
39import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
40import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
41import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
42import org.eclipse.text.edits.TextEditGroup;
43
44import java.util.ArrayList;
45import java.util.List;
46import java.util.TreeMap;
47
48/**
49 * Visitor used by {@link ExtractStringRefactoring} to extract a string from an existing
50 * Java source and replace it by an Android XML string reference.
51 *
52 * @see ExtractStringRefactoring#computeJavaChanges
53 */
54class ReplaceStringsVisitor extends ASTVisitor {
55
56    private static final String CLASS_ANDROID_CONTEXT    = "android.content.Context"; //$NON-NLS-1$
57    private static final String CLASS_JAVA_CHAR_SEQUENCE = "java.lang.CharSequence";  //$NON-NLS-1$
58    private static final String CLASS_JAVA_STRING        = "java.lang.String";        //$NON-NLS-1$
59
60
61    private final AST mAst;
62    private final ASTRewrite mRewriter;
63    private final String mOldString;
64    private final String mRQualifier;
65    private final String mXmlId;
66    private final ArrayList<TextEditGroup> mEditGroups;
67
68    public ReplaceStringsVisitor(AST ast,
69            ASTRewrite astRewrite,
70            ArrayList<TextEditGroup> editGroups,
71            String oldString,
72            String rQualifier,
73            String xmlId) {
74        mAst = ast;
75        mRewriter = astRewrite;
76        mEditGroups = editGroups;
77        mOldString = oldString;
78        mRQualifier = rQualifier;
79        mXmlId = xmlId;
80    }
81
82    @SuppressWarnings("unchecked")
83    @Override
84    public boolean visit(StringLiteral node) {
85        if (node.getLiteralValue().equals(mOldString)) {
86
87            // We want to analyze the calling context to understand whether we can
88            // just replace the string literal by the named int constant (R.id.foo)
89            // or if we should generate a Context.getString() call.
90            boolean useGetResource = false;
91            useGetResource = examineVariableDeclaration(node) ||
92                                examineMethodInvocation(node) ||
93                                examineAssignment(node);
94
95            Name qualifierName = mAst.newName(mRQualifier + ".string");     //$NON-NLS-1$
96            SimpleName idName = mAst.newSimpleName(mXmlId);
97            ASTNode newNode = mAst.newQualifiedName(qualifierName, idName);
98            boolean disabledChange = false;
99            String title = "Replace string by ID";
100
101            if (useGetResource) {
102                Expression context = methodHasContextArgument(node);
103                if (context == null && !isClassDerivedFromContext(node)) {
104                    // if we don't have a class that derives from Context and
105                    // we don't have a Context method argument, then try a bit harder:
106                    // can we find a method or a field that will give us a context?
107                    context = findContextFieldOrMethod(node);
108
109                    if (context == null) {
110                        // If not, let's  write Context.getString(), which is technically
111                        // invalid but makes it a good clue on how to fix it. Since these
112                        // will not compile, we create a disabled change by default.
113                        context = mAst.newSimpleName("Context");            //$NON-NLS-1$
114                        disabledChange = true;
115                    }
116                }
117
118                MethodInvocation mi2 = mAst.newMethodInvocation();
119                mi2.setName(mAst.newSimpleName("getString"));               //$NON-NLS-1$
120                mi2.setExpression(context);
121                mi2.arguments().add(newNode);
122
123                newNode = mi2;
124                title = "Replace string by Context.getString(R.string...)";
125            }
126
127            TextEditGroup editGroup = new EnabledTextEditGroup(title, !disabledChange);
128            mEditGroups.add(editGroup);
129            mRewriter.replace(node, newNode, editGroup);
130        }
131        return super.visit(node);
132    }
133
134    /**
135     * Examines if the StringLiteral is part of an assignment corresponding to the
136     * a string variable declaration, e.g. String foo = id.
137     *
138     * The parent fragment is of syntax "var = expr" or "var[] = expr".
139     * We want the type of the variable, which is either held by a
140     * VariableDeclarationStatement ("type [fragment]") or by a
141     * VariableDeclarationExpression. In either case, the type can be an array
142     * but for us all that matters is to know whether the type is an int or
143     * a string.
144     */
145    private boolean examineVariableDeclaration(StringLiteral node) {
146        VariableDeclarationFragment fragment = findParentClass(node,
147                VariableDeclarationFragment.class);
148
149        if (fragment != null) {
150            ASTNode parent = fragment.getParent();
151
152            Type type = null;
153            if (parent instanceof VariableDeclarationStatement) {
154                type = ((VariableDeclarationStatement) parent).getType();
155            } else if (parent instanceof VariableDeclarationExpression) {
156                type = ((VariableDeclarationExpression) parent).getType();
157            }
158
159            if (type instanceof SimpleType) {
160                return isJavaString(type.resolveBinding());
161            }
162        }
163
164        return false;
165    }
166
167    /**
168     * Examines if the StringLiteral is part of a assignment to a variable that
169     * is a string. We need to lookup the variable to find its type, either in the
170     * enclosing method or class type.
171     */
172    private boolean examineAssignment(StringLiteral node) {
173
174        Assignment assignment = findParentClass(node, Assignment.class);
175        if (assignment != null) {
176            Expression left = assignment.getLeftHandSide();
177
178            ITypeBinding typeBinding = left.resolveTypeBinding();
179            return isJavaString(typeBinding);
180        }
181
182        return false;
183    }
184
185    /**
186     * If the expression is part of a method invocation (aka a function call) or a
187     * class instance creation (aka a "new SomeClass" constructor call), we try to
188     * find the type of the argument being used. If it is a String (most likely), we
189     * want to return true (to generate a getString() call). However if there might
190     * be a similar method that takes an int, in which case we don't want to do that.
191     *
192     * This covers the case of Activity.setTitle(int resId) vs setTitle(String str).
193     */
194    @SuppressWarnings("rawtypes")
195    private boolean examineMethodInvocation(StringLiteral node) {
196
197        ASTNode parent = null;
198        List arguments = null;
199        IMethodBinding methodBinding = null;
200
201        MethodInvocation invoke = findParentClass(node, MethodInvocation.class);
202        if (invoke != null) {
203            parent = invoke;
204            arguments = invoke.arguments();
205            methodBinding = invoke.resolveMethodBinding();
206        } else {
207            ClassInstanceCreation newclass = findParentClass(node, ClassInstanceCreation.class);
208            if (newclass != null) {
209                parent = newclass;
210                arguments = newclass.arguments();
211                methodBinding = newclass.resolveConstructorBinding();
212            }
213        }
214
215        if (parent != null && arguments != null && methodBinding != null) {
216            // We want to know which argument this is.
217            // Walk up the hierarchy again to find the immediate child of the parent,
218            // which should turn out to be one of the invocation arguments.
219            ASTNode child = null;
220            for (ASTNode n = node; n != parent; ) {
221                ASTNode p = n.getParent();
222                if (p == parent) {
223                    child = n;
224                    break;
225                }
226                n = p;
227            }
228            if (child == null) {
229                // This can't happen: a parent of 'node' must be the child of 'parent'.
230                return false;
231            }
232
233            // Find the index
234            int index = 0;
235            for (Object arg : arguments) {
236                if (arg == child) {
237                    break;
238                }
239                index++;
240            }
241
242            if (index == arguments.size()) {
243                // This can't happen: one of the arguments of 'invoke' must be 'child'.
244                return false;
245            }
246
247            // Eventually we want to determine if the parameter is a string type,
248            // in which case a Context.getString() call must be generated.
249            boolean useStringType = false;
250
251            // Find the type of that argument
252            ITypeBinding[] types = methodBinding.getParameterTypes();
253            if (index < types.length) {
254                ITypeBinding type = types[index];
255                useStringType = isJavaString(type);
256            }
257
258            // Now that we know that this method takes a String parameter, can we find
259            // a variant that would accept an int for the same parameter position?
260            if (useStringType) {
261                String name = methodBinding.getName();
262                ITypeBinding clazz = methodBinding.getDeclaringClass();
263                nextMethod: for (IMethodBinding mb2 : clazz.getDeclaredMethods()) {
264                    if (methodBinding == mb2 || !mb2.getName().equals(name)) {
265                        continue;
266                    }
267                    // We found a method with the same name. We want the same parameters
268                    // except that the one at 'index' must be an int type.
269                    ITypeBinding[] types2 = mb2.getParameterTypes();
270                    int len2 = types2.length;
271                    if (types.length == len2) {
272                        for (int i = 0; i < len2; i++) {
273                            if (i == index) {
274                                ITypeBinding type2 = types2[i];
275                                if (!("int".equals(type2.getQualifiedName()))) {   //$NON-NLS-1$
276                                    // The argument at 'index' is not an int.
277                                    continue nextMethod;
278                                }
279                            } else if (!types[i].equals(types2[i])) {
280                                // One of the other arguments do not match our original method
281                                continue nextMethod;
282                            }
283                        }
284                        // If we got here, we found a perfect match: a method with the same
285                        // arguments except the one at 'index' is an int. In this case we
286                        // don't need to convert our R.id into a string.
287                        useStringType = false;
288                        break;
289                    }
290                }
291            }
292
293            return useStringType;
294        }
295        return false;
296    }
297
298    /**
299     * Examines if the StringLiteral is part of a method declaration (a.k.a. a function
300     * definition) which takes a Context argument.
301     * If such, it returns the name of the variable as a {@link SimpleName}.
302     * Otherwise it returns null.
303     */
304    private SimpleName methodHasContextArgument(StringLiteral node) {
305        MethodDeclaration decl = findParentClass(node, MethodDeclaration.class);
306        if (decl != null) {
307            for (Object obj : decl.parameters()) {
308                if (obj instanceof SingleVariableDeclaration) {
309                    SingleVariableDeclaration var = (SingleVariableDeclaration) obj;
310                    if (isAndroidContext(var.getType())) {
311                        return mAst.newSimpleName(var.getName().getIdentifier());
312                    }
313                }
314            }
315        }
316        return null;
317    }
318
319    /**
320     * Walks up the node hierarchy to find the class (aka type) where this statement
321     * is used and returns true if this class derives from android.content.Context.
322     */
323    private boolean isClassDerivedFromContext(StringLiteral node) {
324        TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class);
325        if (clazz != null) {
326            // This is the class that the user is currently writing, so it can't be
327            // a Context by itself, it has to be derived from it.
328            return isAndroidContext(clazz.getSuperclassType());
329        }
330        return false;
331    }
332
333    private Expression findContextFieldOrMethod(StringLiteral node) {
334        TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class);
335        return clazz == null ? null : findContextFieldOrMethod(clazz.resolveBinding());
336    }
337
338    private Expression findContextFieldOrMethod(ITypeBinding clazzType) {
339        TreeMap<Integer, Expression> results = new TreeMap<Integer, Expression>();
340        findContextCandidates(results, clazzType, 0 /*superType*/);
341        if (results.size() > 0) {
342            Integer bestRating = results.keySet().iterator().next();
343            return results.get(bestRating);
344        }
345        return null;
346    }
347
348    /**
349     * Find all method or fields that are candidates for providing a Context.
350     * There can be various choices amongst this class or its super classes.
351     * Sort them by rating in the results map.
352     *
353     * The best ever choice is to find a method with no argument that returns a Context.
354     * The second suitable choice is to find a Context field.
355     * The least desirable choice is to find a method with arguments. It's not really
356     * desirable since we can't generate these arguments automatically.
357     *
358     * Methods and fields from supertypes are ignored if they are private.
359     *
360     * The rating is reversed: the lowest rating integer is used for the best candidate.
361     * Because the superType argument is actually a recursion index, this makes the most
362     * immediate classes more desirable.
363     *
364     * @param results The map that accumulates the rating=>expression results. The lower
365     *                rating number is the best candidate.
366     * @param clazzType The class examined.
367     * @param superType The recursion index.
368     *                  0 for the immediate class, 1 for its super class, etc.
369     */
370    private void findContextCandidates(TreeMap<Integer, Expression> results,
371            ITypeBinding clazzType,
372            int superType) {
373        for (IMethodBinding mb : clazzType.getDeclaredMethods()) {
374            // If we're looking at supertypes, we can't use private methods.
375            if (superType != 0 && Modifier.isPrivate(mb.getModifiers())) {
376                continue;
377            }
378
379            if (isAndroidContext(mb.getReturnType())) {
380                // We found a method that returns something derived from Context.
381
382                int argsLen = mb.getParameterTypes().length;
383                if (argsLen == 0) {
384                    // We'll favor any method that takes no argument,
385                    // That would be the best candidate ever, so we can stop here.
386                    MethodInvocation mi = mAst.newMethodInvocation();
387                    mi.setName(mAst.newSimpleName(mb.getName()));
388                    results.put(Integer.MIN_VALUE, mi);
389                    return;
390                } else {
391                    // A method with arguments isn't as interesting since we wouldn't
392                    // know how to populate such arguments. We'll use it if there are
393                    // no other alternatives. We'll favor the one with the less arguments.
394                    Integer rating = Integer.valueOf(10000 + 1000 * superType + argsLen);
395                    if (!results.containsKey(rating)) {
396                        MethodInvocation mi = mAst.newMethodInvocation();
397                        mi.setName(mAst.newSimpleName(mb.getName()));
398                        results.put(rating, mi);
399                    }
400                }
401            }
402        }
403
404        // A direct Context field would be more interesting than a method with
405        // arguments. Try to find one.
406        for (IVariableBinding var : clazzType.getDeclaredFields()) {
407            // If we're looking at supertypes, we can't use private field.
408            if (superType != 0 && Modifier.isPrivate(var.getModifiers())) {
409                continue;
410            }
411
412            if (isAndroidContext(var.getType())) {
413                // We found such a field. Let's use it.
414                Integer rating = Integer.valueOf(superType);
415                results.put(rating, mAst.newSimpleName(var.getName()));
416                break;
417            }
418        }
419
420        // Examine the super class to see if we can locate a better match
421        clazzType = clazzType.getSuperclass();
422        if (clazzType != null) {
423            findContextCandidates(results, clazzType, superType + 1);
424        }
425    }
426
427    /**
428     * Walks up the node hierarchy and returns the first ASTNode of the requested class.
429     * Only look at parents.
430     *
431     * Implementation note: this is a generic method so that it returns the node already
432     * casted to the requested type.
433     */
434    @SuppressWarnings("unchecked")
435    private <T extends ASTNode> T findParentClass(ASTNode node, Class<T> clazz) {
436        if (node != null) {
437            for (node = node.getParent(); node != null; node = node.getParent()) {
438                if (node.getClass().equals(clazz)) {
439                    return (T) node;
440                }
441            }
442        }
443        return null;
444    }
445
446    /**
447     * Returns true if the given type is or derives from android.content.Context.
448     */
449    private boolean isAndroidContext(Type type) {
450        if (type != null) {
451            return isAndroidContext(type.resolveBinding());
452        }
453        return false;
454    }
455
456    /**
457     * Returns true if the given type is or derives from android.content.Context.
458     */
459    private boolean isAndroidContext(ITypeBinding type) {
460        for (; type != null; type = type.getSuperclass()) {
461            if (CLASS_ANDROID_CONTEXT.equals(type.getQualifiedName())) {
462                return true;
463            }
464        }
465        return false;
466    }
467
468    /**
469     * Returns true if this type binding represents a String or CharSequence type.
470     */
471    private boolean isJavaString(ITypeBinding type) {
472        for (; type != null; type = type.getSuperclass()) {
473            if (CLASS_JAVA_STRING.equals(type.getQualifiedName()) ||
474                CLASS_JAVA_CHAR_SEQUENCE.equals(type.getQualifiedName())) {
475                return true;
476            }
477        }
478        return false;
479    }
480}
481