blob: 66eafaa44b0cea43224d138c6d9f5f50a6561125 [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.java.libraries;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.idea.blaze.base.filecache.FileCache;
import com.google.idea.blaze.base.filecache.FileDiffer;
import com.google.idea.blaze.base.io.FileSizeScanner;
import com.google.idea.blaze.base.model.BlazeLibrary;
import com.google.idea.blaze.base.model.BlazeProjectData;
import com.google.idea.blaze.base.prefetch.FetchExecutor;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.scope.output.PrintOutput;
import com.google.idea.blaze.base.settings.BlazeImportSettings;
import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
import com.google.idea.blaze.base.sync.BlazeSyncParams;
import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
import com.google.idea.blaze.base.sync.libraries.BlazeLibraryCollector;
import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
import com.google.idea.blaze.java.settings.BlazeJavaUserSettings;
import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/** Local cache of the jars referenced by the project. */
public class JarCache {
private static final Logger logger = Logger.getInstance(JarCache.class);
private final Project project;
private final BlazeImportSettings importSettings;
private final File cacheDir;
private boolean enabled;
@Nullable private BiMap<File, String> sourceFileToCacheKey = null;
public static JarCache getInstance(Project project) {
return ServiceManager.getService(project, JarCache.class);
}
public JarCache(Project project) {
this.project = project;
this.importSettings = BlazeImportSettingsManager.getInstance(project).getImportSettings();
this.cacheDir = getCacheDir();
}
public void onSync(
BlazeContext context,
ProjectViewSet projectViewSet,
BlazeProjectData projectData,
BlazeSyncParams.SyncMode syncMode) {
Collection<BlazeLibrary> libraries =
BlazeLibraryCollector.getLibraries(projectViewSet, projectData);
boolean fullRefresh = syncMode == SyncMode.FULL;
boolean removeMissingFiles = syncMode == SyncMode.INCREMENTAL;
boolean enabled = updateEnabled();
if (!enabled || fullRefresh) {
clearCache();
}
if (!enabled) {
return;
}
boolean attachAllSourceJars = BlazeJavaUserSettings.getInstance().getAttachSourcesByDefault();
SourceJarManager sourceJarManager = SourceJarManager.getInstance(project);
List<BlazeJarLibrary> jarLibraries =
libraries
.stream()
.filter(library -> library instanceof BlazeJarLibrary)
.map(library -> (BlazeJarLibrary) library)
.collect(Collectors.toList());
ArtifactLocationDecoder artifactLocationDecoder = projectData.artifactLocationDecoder;
BiMap<File, String> sourceFileToCacheKey = HashBiMap.create(jarLibraries.size());
for (BlazeJarLibrary library : jarLibraries) {
File jarFile =
artifactLocationDecoder.decode(library.libraryArtifact.jarForIntellijLibrary());
sourceFileToCacheKey.put(jarFile, cacheKeyForJar(jarFile));
boolean attachSourceJar =
attachAllSourceJars || sourceJarManager.hasSourceJarAttached(library.key);
if (attachSourceJar && library.libraryArtifact.sourceJar != null) {
File srcJarFile = artifactLocationDecoder.decode(library.libraryArtifact.sourceJar);
sourceFileToCacheKey.put(srcJarFile, cacheKeyForSourceJar(srcJarFile));
}
}
this.sourceFileToCacheKey = sourceFileToCacheKey;
refresh(context, removeMissingFiles);
}
public boolean isEnabled() {
return enabled;
}
private boolean updateEnabled() {
this.enabled =
BlazeJavaUserSettings.getInstance().getUseJarCache()
&& !ApplicationManager.getApplication().isUnitTestMode();
return enabled;
}
/** Refreshes any updated files in the cache. Does not add or removes any files */
public void refresh() {
refresh(null, false);
}
private void refresh(@Nullable BlazeContext context, boolean removeMissingFiles) {
if (!enabled || sourceFileToCacheKey == null) {
return;
}
// Ensure the cache dir exists
if (!cacheDir.exists()) {
if (!cacheDir.mkdirs()) {
logger.error("Could not create jar cache directory");
return;
}
}
// Discover state of source jars
ImmutableMap<File, Long> sourceFileTimestamps =
FileDiffer.readFileState(sourceFileToCacheKey.keySet());
if (sourceFileTimestamps == null) {
return;
}
ImmutableMap.Builder<String, Long> sourceFileCacheKeyToTimestamp = ImmutableMap.builder();
for (Map.Entry<File, Long> entry : sourceFileTimestamps.entrySet()) {
String cacheKey = sourceFileToCacheKey.get(entry.getKey());
sourceFileCacheKeyToTimestamp.put(cacheKey, entry.getValue());
}
// Discover current on-disk cache state
File[] cacheFiles = cacheDir.listFiles();
assert cacheFiles != null;
ImmutableMap<File, Long> cacheFileTimestamps =
FileDiffer.readFileState(Lists.newArrayList(cacheFiles));
if (cacheFileTimestamps == null) {
return;
}
ImmutableMap.Builder<String, Long> cachedFileCacheKeyToTimestamp = ImmutableMap.builder();
for (Map.Entry<File, Long> entry : cacheFileTimestamps.entrySet()) {
String cacheKey = entry.getKey().getName(); // Cache key == file name
cachedFileCacheKeyToTimestamp.put(cacheKey, entry.getValue());
}
List<String> updatedFiles = Lists.newArrayList();
List<String> removedFiles = Lists.newArrayList();
FileDiffer.diffState(
cachedFileCacheKeyToTimestamp.build(),
sourceFileCacheKeyToTimestamp.build(),
updatedFiles,
removedFiles);
ListeningExecutorService executor = FetchExecutor.EXECUTOR;
List<ListenableFuture<?>> futures = Lists.newArrayList();
Map<String, File> cacheKeyToSourceFile = sourceFileToCacheKey.inverse();
for (String cacheKey : updatedFiles) {
File sourceFile = cacheKeyToSourceFile.get(cacheKey);
File cacheFile = cacheFileForKey(cacheKey);
futures.add(
executor.submit(
() -> {
try {
Files.copy(
Paths.get(sourceFile.getPath()),
Paths.get(cacheFile.getPath()),
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
} catch (IOException e) {
logger.warn(e);
}
}));
}
if (removeMissingFiles) {
for (String cacheKey : removedFiles) {
File cacheFile = cacheFileForKey(cacheKey);
futures.add(
executor.submit(
() -> {
try {
Files.deleteIfExists(Paths.get(cacheFile.getPath()));
} catch (IOException e) {
logger.warn(e);
}
}));
}
}
try {
Futures.allAsList(futures).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn(e);
} catch (ExecutionException e) {
logger.error(e);
}
if (context != null && updatedFiles.size() > 0) {
context.output(PrintOutput.log(String.format("Copied %d jars", updatedFiles.size())));
}
if (context != null && removedFiles.size() > 0 && removeMissingFiles) {
context.output(PrintOutput.log(String.format("Removed %d jars", removedFiles.size())));
}
if (context != null) {
try {
File[] finalCacheFiles = cacheDir.listFiles();
assert finalCacheFiles != null;
ImmutableMap<File, Long> cacheFileSizes =
FileSizeScanner.readFilesizes(Lists.newArrayList(finalCacheFiles));
Long total =
cacheFileSizes.values().stream().reduce((size1, size2) -> size1 + size2).orElse(0L);
context.output(
PrintOutput.log(
String.format(
"Total Jar Cache size: %d kB (%d files)",
total / 1024, finalCacheFiles.length)));
} catch (Exception e) {
logger.warn("Could not determine cache size", e);
}
}
}
private void clearCache() {
if (cacheDir.exists()) {
File[] cacheFiles = cacheDir.listFiles();
if (cacheFiles != null) {
FileUtil.asyncDelete(Lists.newArrayList(cacheFiles));
}
}
sourceFileToCacheKey = null;
}
/** Gets the cached file for a jar. If it doesn't exist, we return the file from the library. */
public File getCachedJar(ArtifactLocationDecoder decoder, BlazeJarLibrary library) {
File file = decoder.decode(library.libraryArtifact.jarForIntellijLibrary());
if (!enabled || sourceFileToCacheKey == null) {
return file;
}
String cacheKey = sourceFileToCacheKey.get(file);
if (cacheKey == null) {
return file;
}
return cacheFileForKey(cacheKey);
}
/** Gets the cached file for a source jar. */
@Nullable
public File getCachedSourceJar(ArtifactLocationDecoder decoder, BlazeJarLibrary library) {
if (library.libraryArtifact.sourceJar == null) {
return null;
}
File file = decoder.decode(library.libraryArtifact.sourceJar);
if (!enabled || sourceFileToCacheKey == null) {
return file;
}
String cacheKey = sourceFileToCacheKey.get(file);
if (cacheKey == null) {
return file;
}
return cacheFileForKey(cacheKey);
}
private static String cacheKeyInternal(File jar) {
int parentHash = jar.getParent().hashCode();
return FileUtil.getNameWithoutExtension(jar) + "_" + Integer.toHexString(parentHash);
}
private static String cacheKeyForJar(File jar) {
return cacheKeyInternal(jar) + ".jar";
}
private static String cacheKeyForSourceJar(File srcjar) {
return cacheKeyInternal(srcjar) + "-src.jar";
}
private File cacheFileForKey(String key) {
return new File(cacheDir, key);
}
private File getCacheDir() {
return new File(BlazeDataStorage.getProjectDataDir(importSettings), "libraries");
}
static class FileCacheAdapter implements FileCache {
@Override
public String getName() {
return "Jar Cache";
}
@Override
public void onSync(
Project project,
BlazeContext context,
ProjectViewSet projectViewSet,
BlazeProjectData projectData,
BlazeSyncParams.SyncMode syncMode) {
getInstance(project).onSync(context, projectViewSet, projectData, syncMode);
}
@Override
public void refreshFiles(Project project) {
getInstance(project).refresh();
}
}
}