feat(plugins): cache available plugin list

This commit is contained in:
Skylot 2023-09-26 21:26:01 +01:00
parent b70276d896
commit 89acf73010
No known key found for this signature in database
GPG Key ID: 47866607B16F25C8
5 changed files with 151 additions and 54 deletions

View File

@ -236,12 +236,12 @@ public class FileUtils {
public static void writeFile(Path file, String data) throws IOException {
FileUtils.makeDirsForFile(file);
Files.write(file, data.getBytes(StandardCharsets.UTF_8),
Files.writeString(file, data, StandardCharsets.UTF_8,
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
public static String readFile(Path textFile) throws IOException {
return new String(Files.readAllBytes(textFile), StandardCharsets.UTF_8);
return Files.readString(textFile);
}
@NotNull

View File

@ -5,6 +5,7 @@ import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@ -38,6 +39,7 @@ import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.plugins.tools.JadxPluginsList;
import jadx.plugins.tools.JadxPluginsTools;
import jadx.plugins.tools.data.JadxPluginMetadata;
@ -89,30 +91,12 @@ class PluginSettingsGroup implements ISettingsGroup {
actionsPanel.add(Box.createRigidArea(new Dimension(5, 0)));
actionsPanel.add(updateAllBtn);
List<JadxPluginMetadata> installed = JadxPluginsTools.getInstance().getInstalled();
Map<String, JadxPluginMetadata> installedMap = new HashMap<>(installed.size());
installed.forEach(p -> installedMap.put(p.getPluginId(), p));
List<BasePluginListNode> nodes = new ArrayList<>(installed.size() + 3);
for (PluginContext plugin : installedPlugins) {
nodes.add(new InstalledPluginNode(plugin, installedMap.get(plugin.getPluginId())));
}
nodes.sort(Comparator.comparing(BasePluginListNode::getTitle));
DefaultListModel<BasePluginListNode> listModel = new DefaultListModel<>();
listModel.addElement(new TitleNode("Installed"));
nodes.stream().filter(n -> n.getVersion() != null).forEach(listModel::addElement);
listModel.addElement(new TitleNode("Bundled"));
nodes.stream().filter(n -> n.getVersion() == null).forEach(listModel::addElement);
listModel.addElement(new TitleNode("Available"));
JList<BasePluginListNode> pluginList = new JList<>(listModel);
pluginList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
pluginList.setCellRenderer(new PluginsListCellRenderer());
pluginList.addListSelectionListener(ev -> onSelection(pluginList.getSelectedValue()));
loadAvailablePlugins(listModel, installedPlugins);
JScrollPane scrollPane = new JScrollPane(pluginList);
scrollPane.setMinimumSize(new Dimension(80, 120));
@ -132,29 +116,57 @@ class PluginSettingsGroup implements ISettingsGroup {
mainPanel.setBorder(BorderFactory.createTitledBorder(title));
mainPanel.add(actionsPanel, BorderLayout.PAGE_START);
mainPanel.add(splitPanel, BorderLayout.CENTER);
applyData(listModel);
return mainPanel;
}
private void loadAvailablePlugins(DefaultListModel<BasePluginListNode> listModel, List<PluginContext> installedPlugins) {
List<AvailablePluginNode> list = new ArrayList<>();
private void applyData(DefaultListModel<BasePluginListNode> listModel) {
List<JadxPluginMetadata> installed = JadxPluginsTools.getInstance().getInstalled();
Map<String, JadxPluginMetadata> installedMap = new HashMap<>(installed.size());
installed.forEach(p -> installedMap.put(p.getPluginId(), p));
List<BasePluginListNode> nodes = new ArrayList<>(installed.size() + 3);
for (PluginContext plugin : installedPlugins) {
nodes.add(new InstalledPluginNode(plugin, installedMap.get(plugin.getPluginId())));
}
nodes.sort(Comparator.comparing(BasePluginListNode::getTitle));
fillListModel(listModel, nodes, Collections.emptyList());
loadAvailablePlugins(listModel, nodes, installedPlugins);
}
private static void fillListModel(DefaultListModel<BasePluginListNode> listModel,
List<BasePluginListNode> nodes, List<AvailablePluginNode> available) {
listModel.clear();
listModel.addElement(new TitleNode("Installed"));
nodes.stream().filter(n -> n.getVersion() != null).forEach(listModel::addElement);
listModel.addElement(new TitleNode("Bundled"));
nodes.stream().filter(n -> n.getVersion() == null).forEach(listModel::addElement);
listModel.addElement(new TitleNode("Available"));
listModel.addAll(available);
}
private void loadAvailablePlugins(DefaultListModel<BasePluginListNode> listModel,
List<BasePluginListNode> nodes, List<PluginContext> installedPlugins) {
mainWindow.getBackgroundExecutor().execute(
NLS.str("preferences.plugins.task.downloading_list"),
() -> {
List<JadxPluginMetadata> availablePlugins;
try {
availablePlugins = JadxPluginsList.getInstance().fetch();
JadxPluginsList.getInstance().get(availablePlugins -> {
Set<String> installed = installedPlugins.stream()
.map(PluginContext::getPluginId)
.collect(Collectors.toSet());
List<AvailablePluginNode> availableNodes = availablePlugins.stream()
.filter(availablePlugin -> !installed.contains(availablePlugin.getPluginId()))
.map(AvailablePluginNode::new)
.collect(Collectors.toList());
UiUtils.uiRunAndWait(() -> fillListModel(listModel, nodes, availableNodes));
});
} catch (Exception e) {
LOG.warn("Failed to load available plugins list", e);
return;
}
Set<String> installed = installedPlugins.stream().map(PluginContext::getPluginId).collect(Collectors.toSet());
for (JadxPluginMetadata availablePlugin : availablePlugins) {
if (!installed.contains(availablePlugin.getPluginId())) {
list.add(new AvailablePluginNode(availablePlugin));
}
}
},
status -> listModel.addAll(list));
});
}
private void onSelection(BasePluginListNode node) {

View File

@ -3,18 +3,22 @@ package jadx.plugins.tools;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.utils.files.FileUtils;
import jadx.plugins.tools.data.JadxPluginListCache;
import jadx.plugins.tools.data.JadxPluginMetadata;
import jadx.plugins.tools.resolvers.github.GithubTools;
import jadx.plugins.tools.resolvers.github.LocationInfo;
@ -22,6 +26,8 @@ import jadx.plugins.tools.resolvers.github.data.Asset;
import jadx.plugins.tools.resolvers.github.data.Release;
import jadx.plugins.tools.utils.PluginUtils;
import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_LIST_CACHE;
/**
* TODO: implement list caching (on disk) with check for new release
*/
@ -31,32 +37,96 @@ public class JadxPluginsList {
private static final Type LIST_TYPE = new TypeToken<List<JadxPluginMetadata>>() {
}.getType();
private static final Type CACHE_TYPE = new TypeToken<JadxPluginListCache>() {
}.getType();
public static JadxPluginsList getInstance() {
return INSTANCE;
}
private @Nullable List<JadxPluginMetadata> cache;
private @Nullable JadxPluginListCache loadedList;
private JadxPluginsList() {
}
public synchronized List<JadxPluginMetadata> fetch() {
if (cache != null) {
return cache;
/**
* List provider with update callback.
* Can be called one or two times:
* <br>
* - Apply cached data first
* <br>
* - If update is available, apply data after fetch
* <br>
* Method call is blocking.
*/
public synchronized void get(Consumer<List<JadxPluginMetadata>> consumer) {
if (loadedList != null) {
consumer.accept(loadedList.getList());
return;
}
JadxPluginListCache listCache = loadCache();
if (listCache != null) {
consumer.accept(listCache.getList());
}
Release release = fetchLatestRelease();
if (listCache == null || !listCache.getVersion().equals(release.getName())) {
JadxPluginListCache updatedList = fetchBundle(release);
saveCache(updatedList);
consumer.accept(updatedList.getList());
}
}
public List<JadxPluginMetadata> get() {
AtomicReference<List<JadxPluginMetadata>> holder = new AtomicReference<>();
get(holder::set);
return holder.get();
}
private @Nullable JadxPluginListCache loadCache() {
if (!Files.isRegularFile(PLUGINS_LIST_CACHE)) {
return null;
}
try {
String jsonStr = FileUtils.readFile(PLUGINS_LIST_CACHE);
return buildGson().fromJson(jsonStr, CACHE_TYPE);
} catch (Exception e) {
return null;
}
}
private void saveCache(JadxPluginListCache listCache) {
try {
String jsonStr = buildGson().toJson(listCache, CACHE_TYPE);
FileUtils.writeFile(PLUGINS_LIST_CACHE, jsonStr);
} catch (Exception e) {
throw new RuntimeException("Error saving file: " + PLUGINS_LIST_CACHE, e);
}
loadedList = listCache;
}
private static Gson buildGson() {
return new GsonBuilder().setPrettyPrinting().create();
}
private Release fetchLatestRelease() {
LocationInfo latest = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list", null);
Release release = GithubTools.fetchRelease(latest);
List<Asset> assets = release.getAssets();
if (assets.isEmpty()) {
throw new RuntimeException("Release don't have assets");
}
Asset listAsset = assets.get(0);
return release;
}
private JadxPluginListCache fetchBundle(Release release) {
Asset listAsset = release.getAssets().get(0);
Path tmpListFile = FileUtils.createTempFile("list.zip");
PluginUtils.downloadFile(listAsset.getDownloadUrl(), tmpListFile);
List<JadxPluginMetadata> entries = loadListBundle(tmpListFile);
cache = entries;
return entries;
JadxPluginListCache listCache = new JadxPluginListCache();
listCache.setVersion(release.getName());
listCache.setList(loadListBundle(tmpListFile));
return listCache;
}
private static List<JadxPluginMetadata> loadListBundle(Path tmpListFile) {
@ -73,14 +143,4 @@ public class JadxPluginsList {
});
return entries;
}
@TestOnly
public synchronized List<JadxPluginMetadata> fetchFromLocalBundle(Path bundleFile) {
if (cache != null) {
return cache;
}
List<JadxPluginMetadata> entries = loadListBundle(bundleFile);
cache = entries;
return entries;
}
}

View File

@ -0,0 +1,24 @@
package jadx.plugins.tools.data;
import java.util.List;
public class JadxPluginListCache {
private String version;
private List<JadxPluginMetadata> list;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public List<JadxPluginMetadata> getList() {
return list;
}
public void setList(List<JadxPluginMetadata> list) {
this.list = list;
}
}

View File

@ -10,12 +10,13 @@ import static jadx.core.utils.files.FileUtils.makeDirs;
public class PluginFiles {
private static final ProjectDirectories DIRS = ProjectDirectories.from("io.github", "skylot", "jadx");
public static final Path PLUGINS_DIR = Paths.get(DIRS.configDir, "plugins");
private static final Path PLUGINS_DIR = Paths.get(DIRS.configDir, "plugins");
public static final Path PLUGINS_JSON = PLUGINS_DIR.resolve("plugins.json");
public static final Path INSTALLED_DIR = PLUGINS_DIR.resolve("installed");
public static final Path DROPINS_DIR = PLUGINS_DIR.resolve("dropins");
public static final Path CACHE_DIR = Paths.get(DIRS.cacheDir);
private static final Path CACHE_DIR = Paths.get(DIRS.cacheDir);
public static final Path PLUGINS_LIST_CACHE = CACHE_DIR.resolve("plugin-list.json");
static {
makeDirs(INSTALLED_DIR);