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