001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.File; 007import java.io.FilenameFilter; 008import java.io.IOException; 009import java.io.InputStream; 010import java.nio.file.Files; 011import java.nio.file.InvalidPathException; 012import java.util.ArrayList; 013import java.util.Collection; 014import java.util.HashMap; 015import java.util.List; 016import java.util.Map; 017 018import org.openstreetmap.josm.gui.PleaseWaitRunnable; 019import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 020import org.openstreetmap.josm.gui.progress.ProgressMonitor; 021import org.openstreetmap.josm.io.OsmTransferException; 022import org.openstreetmap.josm.tools.Logging; 023import org.xml.sax.SAXException; 024 025/** 026 * This is an asynchronous task for reading plugin information from the files 027 * in the local plugin repositories. 028 * 029 * It scans the files in the local plugins repository (see {@link org.openstreetmap.josm.data.Preferences#getPluginsDirectory()} 030 * and extracts plugin information from three kind of files: 031 * <ul> 032 * <li>.jar files, assuming that they represent plugin jars</li> 033 * <li>.jar.new files, assuming that these are downloaded but not yet installed plugins</li> 034 * <li>cached lists of available plugins, downloaded for instance from 035 * <a href="https://josm.openstreetmap.de/pluginicons">https://josm.openstreetmap.de/pluginicons</a></li> 036 * </ul> 037 * 038 */ 039public class ReadLocalPluginInformationTask extends PleaseWaitRunnable { 040 private final Map<String, PluginInformation> availablePlugins; 041 private boolean canceled; 042 043 /** 044 * Constructs a new {@code ReadLocalPluginInformationTask}. 045 */ 046 public ReadLocalPluginInformationTask() { 047 super(tr("Reading local plugin information.."), false); 048 availablePlugins = new HashMap<>(); 049 } 050 051 /** 052 * Constructs a new {@code ReadLocalPluginInformationTask}. 053 * @param monitor progress monitor 054 */ 055 public ReadLocalPluginInformationTask(ProgressMonitor monitor) { 056 super(tr("Reading local plugin information.."), monitor, false); 057 availablePlugins = new HashMap<>(); 058 } 059 060 @Override 061 protected void cancel() { 062 canceled = true; 063 } 064 065 @Override 066 protected void finish() { 067 // Do nothing 068 } 069 070 protected void processJarFile(File f, String pluginName) throws PluginException { 071 PluginInformation info = new PluginInformation( 072 f, 073 pluginName 074 ); 075 if (!availablePlugins.containsKey(info.getName())) { 076 info.updateLocalInfo(info); 077 availablePlugins.put(info.getName(), info); 078 } else { 079 PluginInformation current = availablePlugins.get(info.getName()); 080 current.updateFromJar(info); 081 } 082 } 083 084 private static File[] listFiles(File pluginsDirectory, final String regex) { 085 return pluginsDirectory.listFiles((FilenameFilter) (dir, name) -> name.matches(regex)); 086 } 087 088 protected void scanSiteCacheFiles(ProgressMonitor monitor, File pluginsDirectory) { 089 File[] siteCacheFiles = listFiles(pluginsDirectory, "^([0-9]+-)?site.*\\.txt$"); 090 if (siteCacheFiles == null || siteCacheFiles.length == 0) 091 return; 092 monitor.subTask(tr("Processing plugin site cache files...")); 093 monitor.setTicksCount(siteCacheFiles.length); 094 for (File f: siteCacheFiles) { 095 String fname = f.getName(); 096 monitor.setCustomText(tr("Processing file ''{0}''", fname)); 097 try { 098 processLocalPluginInformationFile(f); 099 } catch (PluginListParseException e) { 100 Logging.warn(tr("Failed to scan file ''{0}'' for plugin information. Skipping.", fname)); 101 Logging.error(e); 102 } 103 monitor.worked(1); 104 } 105 } 106 107 protected void scanPluginFiles(ProgressMonitor monitor, File pluginsDirectory) { 108 File[] pluginFiles = pluginsDirectory.listFiles( 109 (FilenameFilter) (dir, name) -> name.endsWith(".jar") || name.endsWith(".jar.new") 110 ); 111 if (pluginFiles == null || pluginFiles.length == 0) 112 return; 113 monitor.subTask(tr("Processing plugin files...")); 114 monitor.setTicksCount(pluginFiles.length); 115 for (File f: pluginFiles) { 116 String fname = f.getName(); 117 monitor.setCustomText(tr("Processing file ''{0}''", fname)); 118 try { 119 if (fname.endsWith(".jar")) { 120 String pluginName = fname.substring(0, fname.length() - 4); 121 processJarFile(f, pluginName); 122 } else if (fname.endsWith(".jar.new")) { 123 String pluginName = fname.substring(0, fname.length() - 8); 124 processJarFile(f, pluginName); 125 } 126 } catch (PluginException e) { 127 Logging.log(Logging.LEVEL_WARN, "PluginException: ", e); 128 Logging.warn(tr("Failed to scan file ''{0}'' for plugin information. Skipping.", fname)); 129 } 130 monitor.worked(1); 131 } 132 } 133 134 protected void scanLocalPluginRepository(ProgressMonitor progressMonitor, File pluginsDirectory) { 135 if (pluginsDirectory == null) 136 return; 137 ProgressMonitor monitor = progressMonitor != null ? progressMonitor : NullProgressMonitor.INSTANCE; 138 try { 139 monitor.beginTask(""); 140 scanSiteCacheFiles(monitor, pluginsDirectory); 141 scanPluginFiles(monitor, pluginsDirectory); 142 } finally { 143 monitor.setCustomText(""); 144 monitor.finishTask(); 145 } 146 } 147 148 protected void processLocalPluginInformationFile(File file) throws PluginListParseException { 149 try (InputStream fin = Files.newInputStream(file.toPath())) { 150 List<PluginInformation> pis = new PluginListParser().parse(fin); 151 for (PluginInformation pi : pis) { 152 // we always keep plugin information from a plugin site because it 153 // includes information not available in the plugin jars Manifest, i.e. 154 // the download link or localized descriptions 155 // 156 availablePlugins.put(pi.name, pi); 157 } 158 } catch (IOException | InvalidPathException e) { 159 throw new PluginListParseException(e); 160 } 161 } 162 163 protected void analyseInProcessPlugins() { 164 for (PluginProxy proxy : PluginHandler.pluginList) { 165 PluginInformation info = proxy.getPluginInformation(); 166 if (canceled) return; 167 if (!availablePlugins.containsKey(info.name)) { 168 availablePlugins.put(info.name, info); 169 } else { 170 availablePlugins.get(info.name).localversion = info.localversion; 171 } 172 } 173 } 174 175 protected void filterOldPlugins() { 176 for (PluginHandler.DeprecatedPlugin p : PluginHandler.DEPRECATED_PLUGINS) { 177 if (canceled) return; 178 if (availablePlugins.containsKey(p.name)) { 179 availablePlugins.remove(p.name); 180 } 181 } 182 } 183 184 @Override 185 protected void realRun() throws SAXException, IOException, OsmTransferException { 186 Collection<String> pluginLocations = PluginInformation.getPluginLocations(); 187 getProgressMonitor().setTicksCount(pluginLocations.size() + 2); 188 if (canceled) return; 189 for (String location : pluginLocations) { 190 scanLocalPluginRepository( 191 getProgressMonitor().createSubTaskMonitor(1, false), 192 new File(location) 193 ); 194 getProgressMonitor().worked(1); 195 if (canceled) return; 196 } 197 analyseInProcessPlugins(); 198 getProgressMonitor().worked(1); 199 if (canceled) return; 200 filterOldPlugins(); 201 getProgressMonitor().worked(1); 202 } 203 204 /** 205 * Replies information about available plugins detected by this task. 206 * 207 * @return information about available plugins detected by this task. 208 */ 209 public List<PluginInformation> getAvailablePlugins() { 210 return new ArrayList<>(availablePlugins.values()); 211 } 212 213 /** 214 * Replies true if the task was canceled by the user 215 * 216 * @return true if the task was canceled by the user 217 */ 218 public boolean isCanceled() { 219 return canceled; 220 } 221}