mirror of
https://github.com/skylot/jadx.git
synced 2024-11-23 12:50:02 +00:00
feat(plugins): cache available plugin list
This commit is contained in:
parent
b70276d896
commit
89acf73010
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user