PY-28016 Package names in the cache are actually matched case-insensitively

It turns out that Gson is able to override even final fields with an
instance created using the default no-args constructor, hence one
has to provide an explicit InstanceCreator for a type to customize the
result value.
This commit is contained in:
Mikhail Golubev
2018-01-15 17:34:40 +03:00
parent 03a1302470
commit 2cb52df739
5 changed files with 54 additions and 11 deletions
@@ -3,7 +3,9 @@ package com.jetbrains.python.packaging;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
@@ -26,10 +28,15 @@ import java.util.TreeMap;
*/
public abstract class PyAbstractPackageCache {
private static final Logger LOG = Logger.getInstance(PyPIPackageCache.class);
private static final Gson ourGson = new GsonBuilder().create();
private static final Gson ourGson = new GsonBuilder()
// Otherwise, GSON uses natural order comparator even for a final TreeMap field
.registerTypeAdapter(new TypeToken<TreeMap<String, PackageInfo>>() { }.getType(),
(InstanceCreator)type -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER))
.create();
@SerializedName("packages")
protected TreeMap<String, PackageInfo> myPackages = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
protected final TreeMap<String, PackageInfo> myPackages = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
protected PyAbstractPackageCache() {
}
@@ -38,21 +45,21 @@ public abstract class PyAbstractPackageCache {
@NotNull
protected static <T extends PyAbstractPackageCache> T load(@NotNull Class<T> classToken,
@NotNull T fallbackValue,
@NotNull String cacheFileName) {
@NotNull Path cacheFilePath) {
T cache = fallbackValue;
try (Reader reader = Files.newBufferedReader(getCachePath(cacheFileName), StandardCharsets.UTF_8)) {
try (Reader reader = Files.newBufferedReader(cacheFilePath, StandardCharsets.UTF_8)) {
cache = ourGson.fromJson(reader, classToken);
LOG.info("Loaded " + cache.getPackageNames().size() + " packages from " + getCachePath(cacheFileName));
LOG.info("Loaded " + cache.getPackageNames().size() + " packages from " + cacheFilePath);
}
catch (IOException exception) {
LOG.warn("Cannot load " + cacheFileName + " package cache from the filesystem", exception);
LOG.warn("Cannot load " + cacheFilePath + " package cache from the filesystem", exception);
}
return cache;
}
protected static void store(@NotNull PyAbstractPackageCache newValue, @NotNull String cacheFileName) {
try {
final Path cacheFilePath = getCachePath(cacheFileName);
final Path cacheFilePath = getDefaultCachePath(cacheFileName);
Files.createDirectories(cacheFilePath.getParent());
try (Writer writer = Files.newBufferedWriter(cacheFilePath, StandardCharsets.UTF_8)) {
ourGson.toJson(newValue, writer);
@@ -64,7 +71,7 @@ public abstract class PyAbstractPackageCache {
}
@NotNull
private static Path getCachePath(@NotNull String cacheFileName) {
protected static Path getDefaultCachePath(@NotNull String cacheFileName) {
return Paths.get(PathManager.getSystemPath(), "python_packages", cacheFileName);
}
@@ -80,7 +87,7 @@ public abstract class PyAbstractPackageCache {
* Checks that the given name is among those available in the repository <em>case-insensitively</em>.
* <p>
* Note that if the cache hasn't been initialized yet or there was an error during its loading,
* {@link #load(Class, PyAbstractPackageCache, String)} returns an empty sentinel value, and, therefore, this method will return {@code false}.
* {@link #load(Class, PyAbstractPackageCache, Path)} returns an empty sentinel value, and, therefore, this method will return {@code false}.
* It's worth writing code analysis so that this value doesn't lead to false positives in the editor
* when the cache is merely not ready.
*
@@ -17,7 +17,7 @@ public class PyCondaPackageCache extends PyAbstractPackageCache {
@NotNull
public static synchronized PyCondaPackageCache getInstance() {
if (ourInstance == null) {
ourInstance = load(PyCondaPackageCache.class, new PyCondaPackageCache(), CACHE_FILE_NAME);
ourInstance = load(PyCondaPackageCache.class, new PyCondaPackageCache(), getDefaultCachePath(CACHE_FILE_NAME));
}
return ourInstance;
}
@@ -1,8 +1,10 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.packaging;
import com.google.common.annotations.VisibleForTesting;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
import java.util.List;
/**
@@ -15,8 +17,14 @@ public class PyPIPackageCache extends PyAbstractPackageCache {
@NotNull
public static synchronized PyPIPackageCache getInstance() {
return getInstance(getDefaultCachePath(CACHE_FILE_NAME));
}
@VisibleForTesting
@NotNull
static synchronized PyPIPackageCache getInstance(@NotNull Path pathToCache) {
if (ourInstance == null) {
ourInstance = PyAbstractPackageCache.load(PyPIPackageCache.class, new PyPIPackageCache(), CACHE_FILE_NAME);
ourInstance = PyAbstractPackageCache.load(PyPIPackageCache.class, new PyPIPackageCache(), pathToCache);
}
return ourInstance;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/
package com.jetbrains.python.packaging;
import com.jetbrains.python.fixtures.PyTestCase;
import java.nio.file.Paths;
/**
* @author Mikhail Golubev
*/
public class PyPackageCacheTest extends PyTestCase {
// PY-28016
public void testCaseInsensitivePackageNameMatching() {
final PyPIPackageCache cache = PyPIPackageCache.getInstance(Paths.get(getTestDataPath(), "pypi-cache.json"));
assertTrue(cache.containsPackage("flask"));
assertTrue(cache.containsPackage("Flask"));
}
@Override
protected String getTestDataPath() {
return super.getTestDataPath() + "/packaging";
}
}