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 public ReadLocalPluginInformationTask(ProgressMonitor monitor) { 050 super(tr("Reading local plugin information.."), monitor, false); 051 availablePlugins = new HashMap<>(); 052 } 053 054 @Override 055 protected void cancel() { 056 canceled = true; 057 } 058 059 @Override 060 protected void finish() { 061 // Do nothing 062 } 063 064 protected void processJarFile(File f, String pluginName) throws PluginException { 065 PluginInformation info = new PluginInformation( 066 f, 067 pluginName 068 ); 069 if (!availablePlugins.containsKey(info.getName())) { 070 info.updateLocalInfo(info); 071 availablePlugins.put(info.getName(), info); 072 } else { 073 PluginInformation current = availablePlugins.get(info.getName()); 074 current.updateFromJar(info); 075 } 076 } 077 078 private static File[] listFiles(File pluginsDirectory, final String regex) { 079 return pluginsDirectory.listFiles( 080 new FilenameFilter() { 081 @Override 082 public boolean accept(File dir, String name) { 083 return name.matches(regex); 084 } 085 } 086 ); 087 } 088 089 protected void scanSiteCacheFiles(ProgressMonitor monitor, File pluginsDirectory) { 090 File[] siteCacheFiles = listFiles(pluginsDirectory, "^([0-9]+-)?site.*\\.txt$"); 091 if (siteCacheFiles == null || siteCacheFiles.length == 0) 092 return; 093 monitor.subTask(tr("Processing plugin site cache files...")); 094 monitor.setTicksCount(siteCacheFiles.length); 095 for (File f: siteCacheFiles) { 096 String fname = f.getName(); 097 monitor.setCustomText(tr("Processing file ''{0}''", fname)); 098 try { 099 processLocalPluginInformationFile(f); 100 } catch (PluginListParseException e) { 101 Main.warn(tr("Failed to scan file ''{0}'' for plugin information. Skipping.", fname)); 102 Main.error(e); 103 } 104 monitor.worked(1); 105 } 106 } 107 108 protected void scanPluginFiles(ProgressMonitor monitor, File pluginsDirectory) { 109 File[] pluginFiles = pluginsDirectory.listFiles( 110 new FilenameFilter() { 111 @Override 112 public boolean accept(File dir, String name) { 113 return name.endsWith(".jar") || name.endsWith(".jar.new"); 114 } 115 } 116 ); 117 if (pluginFiles == null || pluginFiles.length == 0) 118 return; 119 monitor.subTask(tr("Processing plugin files...")); 120 monitor.setTicksCount(pluginFiles.length); 121 for (File f: pluginFiles) { 122 String fname = f.getName(); 123 monitor.setCustomText(tr("Processing file ''{0}''", fname)); 124 try { 125 if (fname.endsWith(".jar")) { 126 String pluginName = fname.substring(0, fname.length() - 4); 127 processJarFile(f, pluginName); 128 } else if (fname.endsWith(".jar.new")) { 129 String pluginName = fname.substring(0, fname.length() - 8); 130 processJarFile(f, pluginName); 131 } 132 } catch (PluginException e) { 133 Main.warn("PluginException: "+e.getMessage()); 134 Main.warn(tr("Failed to scan file ''{0}'' for plugin information. Skipping.", fname)); 135 } 136 monitor.worked(1); 137 } 138 } 139 140 protected void scanLocalPluginRepository(ProgressMonitor monitor, File pluginsDirectory) { 141 if (pluginsDirectory == null) 142 return; 143 if (monitor == null) 144 monitor = NullProgressMonitor.INSTANCE; 145 try { 146 monitor.beginTask(""); 147 scanSiteCacheFiles(monitor, pluginsDirectory); 148 scanPluginFiles(monitor, pluginsDirectory); 149 } finally { 150 monitor.setCustomText(""); 151 monitor.finishTask(); 152 } 153 } 154 155 protected void processLocalPluginInformationFile(File file) throws PluginListParseException { 156 try (FileInputStream fin = new FileInputStream(file)) { 157 List<PluginInformation> pis = new PluginListParser().parse(fin); 158 for (PluginInformation pi : pis) { 159 // we always keep plugin information from a plugin site because it 160 // includes information not available in the plugin jars Manifest, i.e. 161 // the download link or localized descriptions 162 // 163 availablePlugins.put(pi.name, pi); 164 } 165 } catch (IOException e) { 166 throw new PluginListParseException(e); 167 } 168 } 169 170 protected void analyseInProcessPlugins() { 171 for (PluginProxy proxy : PluginHandler.pluginList) { 172 PluginInformation info = proxy.getPluginInformation(); 173 if (canceled) return; 174 if (!availablePlugins.containsKey(info.name)) { 175 availablePlugins.put(info.name, info); 176 } else { 177 availablePlugins.get(info.name).localversion = info.localversion; 178 } 179 } 180 } 181 182 protected void filterOldPlugins() { 183 for (PluginHandler.DeprecatedPlugin p : PluginHandler.DEPRECATED_PLUGINS) { 184 if (canceled) return; 185 if (availablePlugins.containsKey(p.name)) { 186 availablePlugins.remove(p.name); 187 } 188 } 189 } 190 191 @Override 192 protected void realRun() throws SAXException, IOException, OsmTransferException { 193 Collection<String> pluginLocations = PluginInformation.getPluginLocations(); 194 getProgressMonitor().setTicksCount(pluginLocations.size() + 2); 195 if (canceled) return; 196 for (String location : pluginLocations) { 197 scanLocalPluginRepository( 198 getProgressMonitor().createSubTaskMonitor(1, false), 199 new File(location) 200 ); 201 getProgressMonitor().worked(1); 202 if (canceled) return; 203 } 204 analyseInProcessPlugins(); 205 getProgressMonitor().worked(1); 206 if (canceled) return; 207 filterOldPlugins(); 208 getProgressMonitor().worked(1); 209 } 210 211 /** 212 * Replies information about available plugins detected by this task. 213 * 214 * @return information about available plugins detected by this task. 215 */ 216 public List<PluginInformation> getAvailablePlugins() { 217 return new ArrayList<>(availablePlugins.values()); 218 } 219 220 /** 221 * Replies true if the task was canceled by the user 222 * 223 * @return true if the task was canceled by the user 224 */ 225 public boolean isCanceled() { 226 return canceled; 227 } 228}