blob: 8c44c95319e21fcf1f348cb4cb6ea8d2b007c5e6 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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 org.jetbrains.android;
import static com.intellij.util.ArrayUtilRt.find;
import static org.jetbrains.android.facet.LayoutViewClassUtils.getTagNamesByClass;
import com.android.support.AndroidxName;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.psi.TagToClassMapper;
import com.google.common.collect.Maps;
import com.intellij.ProjectTopics;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ModuleRootListener;
import com.intellij.openapi.util.Computable;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.ClassInheritorsSearch;
import com.intellij.psi.util.CachedValue;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.util.messages.MessageBusConnection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.jetbrains.android.refactoring.MigrateToAndroidxUtil;
import org.jetbrains.annotations.NotNull;
class TagToClassMapperImpl implements TagToClassMapper {
private final Map<String, Map<String, SmartPsiElementPointer<PsiClass>>> myInitialClassMaps = new HashMap<>();
private final Map<String, CachedValue<Map<String, PsiClass>>> myClassMaps = Maps.newConcurrentMap();
private final Module myModule;
TagToClassMapperImpl(@NotNull Module module) {
myModule = module;
MessageBusConnection connection = module.getMessageBus().connect(module);
connection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
@Override
public void rootsChanged(@NotNull ModuleRootEvent event) {
// Clear the class inheritance map to make sure new dependencies from libraries are picked up
clear();
}
});
}
/**
* {@inheritDoc}
*
* In addition, a {@link CachedValue} for this mapping is created that is updated automatically with changes
* to {@link PsiModificationTracker#JAVA_STRUCTURE_MODIFICATION_COUNT}.
*/
@Override
@NotNull
public Map<String, PsiClass> getFrameworkClassMap(@NotNull String frameworkClass) {
Map<String, PsiClass> frameworkClasses = getClassMap(frameworkClass);
return Collections.unmodifiableMap(frameworkClasses);
}
@NotNull
@Override
public Map<String, PsiClass> getAndroidXClassMap(@NotNull AndroidxName androidXClass) {
String qualifiedName = MigrateToAndroidxUtil.getNameInProject(androidXClass, myModule.getProject());
Map<String, PsiClass> libClasses = getClassMap(qualifiedName);
return Collections.unmodifiableMap(libClasses);
}
private Map<String, PsiClass> getClassMap(String className) {
CachedValue<Map<String, PsiClass>> value = myClassMaps.get(className);
if (value == null) {
value = CachedValuesManager.getManager(myModule.getProject()).createCachedValue(() -> {
Map<String, PsiClass> map = computeClassMap(className);
return CachedValueProvider.Result.create(map, PsiModificationTracker.JAVA_STRUCTURE_MODIFICATION_COUNT);
}, false);
myClassMaps.put(className, value);
}
return value.getValue();
}
@NotNull
private Map<String, PsiClass> computeClassMap(@NotNull String className) {
Map<String, SmartPsiElementPointer<PsiClass>> classMap = getInitialClassMap(className, false);
Map<String, PsiClass> result = new HashMap<>();
boolean shouldRebuildInitialMap = false;
for (String key : classMap.keySet()) {
SmartPsiElementPointer<PsiClass> pointer = classMap.get(key);
if (!isUpToDate(pointer, key)) {
shouldRebuildInitialMap = true;
break;
}
PsiClass aClass = pointer.getElement();
if (aClass != null) {
result.put(key, aClass);
}
}
if (shouldRebuildInitialMap) {
result.clear();
classMap = getInitialClassMap(className, true);
for (String key : classMap.keySet()) {
SmartPsiElementPointer<PsiClass> pointer = classMap.get(key);
PsiClass aClass = pointer.getElement();
if (aClass != null) {
result.put(key, aClass);
}
}
}
fillMap(className, myModule.getModuleWithDependenciesAndLibrariesScope(true), result, false);
return result;
}
private static boolean isUpToDate(@NotNull SmartPsiElementPointer<PsiClass> pointer, String tagName) {
PsiClass aClass = pointer.getElement();
if (aClass == null) {
return false;
}
String[] tagNames = getTagNamesByClass(aClass, -1);
return find(tagNames, tagName) >= 0;
}
@NotNull
private Map<String, SmartPsiElementPointer<PsiClass>> getInitialClassMap(@NotNull String className, boolean forceRebuild) {
Map<String, SmartPsiElementPointer<PsiClass>> viewClassMap;
viewClassMap = myInitialClassMaps.get(className);
if (viewClassMap != null && !forceRebuild) {
return viewClassMap;
}
Map<String, PsiClass> map = new HashMap<>();
if (fillMap(className, myModule.getModuleWithDependenciesAndLibrariesScope(true), map, true)) {
viewClassMap = new HashMap<>(map.size());
SmartPointerManager manager = SmartPointerManager.getInstance(myModule.getProject());
for (Map.Entry<String, PsiClass> entry : map.entrySet()) {
viewClassMap.put(entry.getKey(), manager.createSmartPsiElementPointer(entry.getValue()));
}
myInitialClassMaps.put(className, viewClassMap);
}
return viewClassMap != null ? viewClassMap : Collections.emptyMap();
}
private boolean fillMap(@NotNull String className,
@NotNull GlobalSearchScope scope,
@NotNull Map<String, PsiClass> map,
boolean libClassesOnly) {
JavaPsiFacade facade = JavaPsiFacade.getInstance(myModule.getProject());
PsiClass baseClass = ApplicationManager.getApplication().runReadAction((Computable<PsiClass>)() -> {
PsiClass aClass;
// facade.findClass uses index to find class by name, which might throw an IndexNotReadyException in dumb mode
try {
aClass = facade.findClass(className, myModule.getModuleWithDependenciesAndLibrariesScope(true));
}
catch (IndexNotReadyException e) {
aClass = null;
}
return aClass;
});
if (baseClass == null) {
return false;
}
AndroidModuleInfo androidModuleInfo = AndroidModuleInfo.getInstance(myModule);
int api = androidModuleInfo == null ? 1 : androidModuleInfo.getModuleMinApi();
String[] baseClassTagNames = getTagNamesByClass(baseClass, api);
for (String tagName : baseClassTagNames) {
map.put(tagName, baseClass);
}
try {
ClassInheritorsSearch.search(baseClass, scope, true).forEach(c -> {
if (libClassesOnly && c.getManager().isInProject(c)) {
return true;
}
String[] tagNames = getTagNamesByClass(c, api);
for (String tagName : tagNames) {
map.put(tagName, c);
}
return true;
});
}
catch (IndexNotReadyException e) {
Logger.getInstance(getClass()).info(e);
return false;
}
return !map.isEmpty();
}
public void clear() {
myInitialClassMaps.clear();
}
}