1/*
2 * Copyright 2014, Google Inc.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 *     * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *     * Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
14 * distribution.
15 *     * Neither the name of Google Inc. nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32package org.jf.smalidea.util;
33
34import com.google.common.collect.ImmutableMap;
35import com.intellij.openapi.project.DumbService;
36import com.intellij.openapi.project.Project;
37import com.intellij.psi.*;
38import com.intellij.psi.impl.ResolveScopeManager;
39import com.intellij.psi.search.GlobalSearchScope;
40import org.jetbrains.annotations.NotNull;
41import org.jetbrains.annotations.Nullable;
42
43import java.util.Map;
44
45public class NameUtils {
46    private static final Map<String, String> javaToSmaliPrimitiveTypes = ImmutableMap.<String, String>builder()
47            .put("boolean", "Z")
48            .put("byte", "B")
49            .put("char", "C")
50            .put("short", "S")
51            .put("int", "I")
52            .put("long", "J")
53            .put("float", "F")
54            .put("double", "D")
55            .build();
56
57    @NotNull
58    public static String javaToSmaliType(@NotNull PsiType psiType) {
59        if (psiType instanceof PsiClassType) {
60            PsiClass psiClass = ((PsiClassType)psiType).resolve();
61            if (psiClass != null) {
62                return javaToSmaliType(psiClass);
63            }
64        }
65        return javaToSmaliType(psiType.getCanonicalText());
66    }
67
68    @NotNull
69    public static String javaToSmaliType(@NotNull PsiClass psiClass) {
70        String qualifiedName = psiClass.getQualifiedName();
71        if (qualifiedName == null) {
72            throw new IllegalArgumentException("This method does not support anonymous classes");
73        }
74        PsiClass parent = psiClass.getContainingClass();
75        if (parent != null) {
76            int offset = qualifiedName.lastIndexOf('.');
77            String parentName = qualifiedName.substring(0, offset);
78            assert parentName.equals(parent.getQualifiedName());
79            String className = qualifiedName.substring(offset+1, qualifiedName.length());
80            assert className.equals(psiClass.getName());
81            return javaToSmaliType(parentName + '$' + className);
82        } else {
83            return javaToSmaliType(psiClass.getQualifiedName());
84        }
85    }
86
87    @NotNull
88    public static String javaToSmaliType(@NotNull String javaType) {
89        if (javaType.charAt(javaType.length()-1) == ']') {
90            int dimensions = 0;
91            int firstArrayChar = -1;
92            for (int i=0; i<javaType.length(); i++) {
93                if (javaType.charAt(i) == '[') {
94                    if (firstArrayChar == -1) {
95                        firstArrayChar = i;
96                    }
97                    dimensions++;
98                }
99            }
100            if (dimensions > 0) {
101                StringBuilder sb = new StringBuilder(firstArrayChar + 2 + dimensions);
102                for (int i=0; i<dimensions; i++) {
103                    sb.append('[');
104                }
105                convertSimpleJavaToSmaliType(javaType.substring(0, firstArrayChar), sb);
106                return sb.toString();
107            }
108        }
109
110        return simpleJavaToSmaliType(javaType);
111    }
112
113    private static void convertSimpleJavaToSmaliType(@NotNull String javaType, @NotNull StringBuilder dest) {
114        String smaliType = javaToSmaliPrimitiveTypes.get(javaType);
115        if (smaliType != null) {
116            dest.append(smaliType);
117        } else {
118            dest.append('L');
119            for (int i=0; i<javaType.length(); i++) {
120                char c = javaType.charAt(i);
121                if (c == '.') {
122                    dest.append('/');
123                } else {
124                    dest.append(c);
125                }
126            }
127            dest.append(';');
128        }
129    }
130
131    public static PsiClass resolveSmaliType(@NotNull Project project, @NotNull GlobalSearchScope scope,
132                                            @NotNull String smaliType) {
133        if (DumbService.isDumb(project)) {
134            return null;
135        }
136
137        JavaPsiFacade facade = JavaPsiFacade.getInstance(project);
138
139        String javaType = NameUtils.smaliToJavaType(smaliType);
140
141        PsiClass psiClass = facade.findClass(javaType, scope);
142        if (psiClass != null) {
143            return psiClass;
144        }
145
146        int offset = javaType.lastIndexOf('.');
147        if (offset < 0) {
148            offset = 0;
149        }
150        // find the first $ after the last .
151        offset = javaType.indexOf('$', offset+1);
152        if (offset < 0) {
153            return null;
154        }
155
156        while (offset > 0 && offset < javaType.length()-1) {
157            String left = javaType.substring(0, offset);
158            psiClass = facade.findClass(left, scope);
159            if (psiClass != null) {
160                psiClass = findInnerClass(psiClass, javaType.substring(offset+1, javaType.length()), facade, scope);
161                if (psiClass != null) {
162                    return psiClass;
163                }
164            }
165            offset = javaType.indexOf('$', offset+1);
166        }
167        return null;
168    }
169
170    @Nullable
171    public static PsiClass resolveSmaliType(@NotNull PsiElement element, @NotNull String smaliType) {
172        GlobalSearchScope scope = ResolveScopeManager.getElementResolveScope(element);
173        return resolveSmaliType(element.getProject(), scope, smaliType);
174    }
175
176    @Nullable
177    public static PsiClass findInnerClass(@NotNull PsiClass outerClass, String innerText, JavaPsiFacade facade,
178                                          GlobalSearchScope scope) {
179        int offset = innerText.indexOf('$');
180        if (offset < 0) {
181            offset = innerText.length();
182        }
183
184        while (offset > 0 && offset <= innerText.length()) {
185            String left = innerText.substring(0, offset);
186            String nextInner = outerClass.getQualifiedName() + "." + left;
187            PsiClass psiClass = facade.findClass(nextInner, scope);
188            if (psiClass != null) {
189                if (offset < innerText.length()) {
190                    psiClass = findInnerClass(psiClass, innerText.substring(offset+1, innerText.length()), facade,
191                            scope);
192                    if (psiClass != null) {
193                        return psiClass;
194                    }
195                } else {
196                    return psiClass;
197                }
198            }
199            if (offset >= innerText.length()) {
200                break;
201            }
202            offset = innerText.indexOf('$', offset+1);
203            if (offset < 0) {
204                offset = innerText.length();
205            }
206        }
207        return null;
208    }
209
210    private static String simpleJavaToSmaliType(@NotNull String simpleJavaType) {
211        StringBuilder sb = new StringBuilder(simpleJavaType.length() + 2);
212        convertSimpleJavaToSmaliType(simpleJavaType, sb);
213        sb.trimToSize();
214        return sb.toString();
215    }
216
217    @NotNull
218    public static String smaliToJavaType(@NotNull String smaliType) {
219        if (smaliType.charAt(0) == '[') {
220            return convertSmaliArrayToJava(smaliType);
221        } else {
222            StringBuilder sb = new StringBuilder(smaliType.length());
223            convertAndAppendNonArraySmaliTypeToJava(smaliType, sb);
224            return sb.toString();
225        }
226    }
227
228    @NotNull
229    public static String resolveSmaliToJavaType(@NotNull Project project, @NotNull GlobalSearchScope scope,
230                                                @NotNull String smaliType) {
231        // First, try to resolve the type and get its qualified name, so that we can make sure
232        // to use the correct name for inner classes
233        PsiClass resolvedType = resolveSmaliType(project, scope, smaliType);
234        if (resolvedType != null) {
235            String qualifiedName = resolvedType.getQualifiedName();
236            if (qualifiedName != null) {
237                return qualifiedName;
238            }
239        }
240
241        // if we can't find it, just do a textual conversion of the name
242        return smaliToJavaType(smaliType);
243    }
244
245    @NotNull
246    public static String resolveSmaliToJavaType(@NotNull PsiElement element, @NotNull String smaliType) {
247        return resolveSmaliToJavaType(element.getProject(), element.getResolveScope(), smaliType);
248    }
249
250    @NotNull
251    public static PsiType resolveSmaliToPsiType(@NotNull PsiElement element, @NotNull String smaliType) {
252        PsiClass resolvedType = resolveSmaliType(element, smaliType);
253        if (resolvedType != null) {
254            PsiElementFactory factory = JavaPsiFacade.getInstance(element.getProject()).getElementFactory();
255            return factory.createType(resolvedType);
256        }
257
258        String javaType = NameUtils.smaliToJavaType(smaliType);
259        PsiElementFactory factory = JavaPsiFacade.getInstance(element.getProject()).getElementFactory();
260        return factory.createTypeFromText(javaType, element);
261    }
262
263    @NotNull
264    private static String convertSmaliArrayToJava(@NotNull String smaliType) {
265        int dimensions=0;
266        while (smaliType.charAt(dimensions) == '[') {
267            dimensions++;
268        }
269
270        StringBuilder sb = new StringBuilder(smaliType.length() + dimensions);
271        convertAndAppendNonArraySmaliTypeToJava(smaliType.substring(dimensions), sb);
272        for (int i=0; i<dimensions; i++) {
273            sb.append("[]");
274        }
275        return sb.toString();
276    }
277
278    private static void convertAndAppendNonArraySmaliTypeToJava(@NotNull String smaliType, @NotNull StringBuilder dest) {
279        switch (smaliType.charAt(0)) {
280            case 'Z':
281                dest.append("boolean");
282                return;
283            case 'B':
284                dest.append("byte");
285                return;
286            case 'C':
287                dest.append("char");
288                return;
289            case 'S':
290                dest.append("short");
291                return;
292            case 'I':
293                dest.append("int");
294                return;
295            case 'J':
296                dest.append("long");
297                return;
298            case 'F':
299                dest.append("float");
300                return;
301            case 'D':
302                dest.append("double");
303            case 'L':
304                for (int i=1; i<smaliType.length()-1; i++) {
305                    char c = smaliType.charAt(i);
306                    if (c == '/') {
307                        dest.append('.');
308                    } else {
309                        dest.append(c);
310                    }
311                }
312                return;
313            case 'V':
314                dest.append("void");
315                return;
316            case 'U':
317                if (smaliType.equals("Ujava/lang/Object;")) {
318                    dest.append("java.lang.Object");
319                    return;
320                }
321                // fall through
322            default:
323                throw new RuntimeException("Invalid smali type: " + smaliType);
324        }
325    }
326
327    @Nullable
328    public static String shortNameFromQualifiedName(@Nullable String qualifiedName) {
329        if (qualifiedName == null) {
330            return null;
331        }
332
333        int index = qualifiedName.lastIndexOf('.');
334        if (index == -1) {
335            return qualifiedName;
336        }
337        return qualifiedName.substring(index+1);
338    }
339}
340