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}