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