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