blob: cbad879c4f7783f1cceb69f03713b638f422749b [file] [log] [blame]
/*
* Copyright 2016 The Bazel Authors. All rights reserved.
*
* 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 com.google.idea.blaze.android.sync.model.idea;
import com.android.SdkConstants;
import com.android.tools.idea.model.ClassJarProvider;
import com.android.tools.idea.res.AppResourceRepository;
import com.android.tools.idea.res.ResourceClassRegistry;
import com.google.common.collect.Lists;
import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
import com.google.idea.blaze.base.ideinfo.AndroidIdeInfo;
import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
import com.google.idea.blaze.base.ideinfo.TargetKey;
import com.google.idea.blaze.base.ideinfo.TargetMap;
import com.google.idea.blaze.base.io.VirtualFileSystemProvider;
import com.google.idea.blaze.base.model.BlazeProjectData;
import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
import com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap;
import com.google.idea.sdkcompat.android.res.AppResourceRepositoryAdapter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.util.containers.OrderedSet;
import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
/** Collects class jars from the user's build. */
public class BlazeClassJarProvider extends ClassJarProvider {
private final Project project;
private final AtomicBoolean pendingJarsRefresh;
public BlazeClassJarProvider(final Project project) {
this.project = project;
this.pendingJarsRefresh = new AtomicBoolean(false);
}
@Override
@Nullable
public VirtualFile findModuleClassFile(String className, Module module) {
BlazeProjectData blazeProjectData =
BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
if (blazeProjectData == null) {
return null;
}
TargetMap targetMap = blazeProjectData.targetMap;
ArtifactLocationDecoder decoder = blazeProjectData.artifactLocationDecoder;
AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
TargetIdeInfo target = blazeProjectData.targetMap.get(registry.getTargetKey(module));
if (target == null || target.javaIdeInfo == null) {
return null;
}
// As a potential optimization, we could choose an arbitrary android_binary target
// that depends on the library to provide a single complete resource jar,
// instead of having to rely on dynamic class generation.
// TODO: benchmark to see if optimization is worthwhile.
String classNamePath = className.replace('.', File.separatorChar) + SdkConstants.DOT_CLASS;
List<LibraryArtifact> jarsToSearch = Lists.newArrayList(target.javaIdeInfo.jars);
jarsToSearch.addAll(
TransitiveDependencyMap.getInstance(project)
.getTransitiveDependencies(target.key)
.stream()
.map(targetMap::get)
.filter(Objects::nonNull)
.flatMap(BlazeClassJarProvider::getNonResourceJars)
.collect(Collectors.toList()));
List<File> missingClassJars = Lists.newArrayList();
for (LibraryArtifact jar : jarsToSearch) {
if (jar.classJar == null || jar.classJar.isSource()) {
continue;
}
File classJarFile = decoder.decode(jar.classJar);
VirtualFile classJarVF =
VirtualFileSystemProvider.getInstance().getSystem().findFileByIoFile(classJarFile);
if (classJarVF == null) {
if (classJarFile.exists()) {
missingClassJars.add(classJarFile);
}
continue;
}
VirtualFile classFile = findClassInJar(classJarVF, classNamePath);
if (classFile != null) {
return classFile;
}
}
maybeRefreshJars(missingClassJars, pendingJarsRefresh);
return null;
}
private static Stream<LibraryArtifact> getNonResourceJars(TargetIdeInfo target) {
if (target.javaIdeInfo == null) {
return null;
}
Stream<LibraryArtifact> jars = target.javaIdeInfo.jars.stream();
if (target.androidIdeInfo != null) {
jars = jars.filter(jar -> !jar.equals(target.androidIdeInfo.resourceJar));
}
return jars;
}
@Nullable
private static VirtualFile findClassInJar(final VirtualFile classJar, String classNamePath) {
VirtualFile jarRoot = getJarRootForLocalFile(classJar);
if (jarRoot == null) {
return null;
}
return jarRoot.findFileByRelativePath(classNamePath);
}
@Override
public List<VirtualFile> getModuleExternalLibraries(Module module) {
OrderedSet<VirtualFile> results = new OrderedSet<>();
BlazeProjectData blazeProjectData =
BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
if (blazeProjectData == null) {
return results;
}
TargetMap targetMap = blazeProjectData.targetMap;
ArtifactLocationDecoder decoder = blazeProjectData.artifactLocationDecoder;
AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
TargetIdeInfo target = targetMap.get(registry.getTargetKey(module));
if (target == null) {
return results;
}
AppResourceRepository repository = AppResourceRepositoryAdapter.getOrCreateInstance(module);
for (TargetKey dependencyTargetKey :
TransitiveDependencyMap.getInstance(project).getTransitiveDependencies(target.key)) {
TargetIdeInfo dependencyTarget = targetMap.get(dependencyTargetKey);
if (dependencyTarget == null) {
continue;
}
// Add all import jars as external libraries.
JavaIdeInfo javaIdeInfo = dependencyTarget.javaIdeInfo;
if (javaIdeInfo != null) {
for (LibraryArtifact jar : javaIdeInfo.jars) {
if (jar.classJar != null && jar.classJar.isSource()) {
VirtualFile classJar =
VirtualFileSystemProvider.getInstance()
.getSystem()
.findFileByIoFile(decoder.decode(jar.classJar));
if (classJar != null) {
results.add(classJar);
}
}
}
}
// Tell ResourceClassRegistry which repository contains our resources and the java packages of
// the resources that we're interested in.
// When the class loader tries to load a custom view, and the view references resource
// classes, layoutlib will ask the class loader for these resource classes.
// If these resource classes are in a separate jar from the target (i.e., in a dependency),
// then offering their jars will lead to a conflict in the resource IDs.
// So instead, the resource class generator will produce dummy resource classes with
// non-conflicting IDs to satisfy the class loader.
// The resource repository remembers the dynamic IDs that it handed out and when the layoutlib
// calls to ask about the name and content of a given resource ID, the repository can just
// answer what it has already stored.
AndroidIdeInfo androidIdeInfo = dependencyTarget.androidIdeInfo;
if (androidIdeInfo != null && repository != null) {
ResourceClassRegistry.get(module.getProject())
.addLibrary(repository, androidIdeInfo.resourceJavaPackage);
}
}
return results;
}
private static void maybeRefreshJars(Collection<File> missingJars, AtomicBoolean pendingRefresh) {
// We probably need to refresh the virtual file system to find these files, but we can't refresh
// here because we're in a read action. We also can't use the async refreshIoFiles since it
// still tries to refresh the IO files synchronously. A global async refresh can't find new
// files in the ObjFS since we're not watching it.
// We need to do our own asynchronous refresh, and guard it with a flag to prevent the event
// queue from overflowing.
if (!missingJars.isEmpty() && !pendingRefresh.getAndSet(true)) {
ApplicationManager.getApplication()
.invokeLater(
() -> {
LocalFileSystem.getInstance().refreshIoFiles(missingJars);
pendingRefresh.set(false);
},
ModalityState.NON_MODAL);
}
}
private static VirtualFile getJarRootForLocalFile(VirtualFile file) {
return ApplicationManager.getApplication().isUnitTestMode()
? TempFileSystem.getInstance().findFileByPath(file.getPath() + JarFileSystem.JAR_SEPARATOR)
: JarFileSystem.getInstance().getJarRootForLocalFile(file);
}
}