001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.plugin;
003
004import java.io.File;
005import java.util.ArrayList;
006import java.util.Collection;
007import java.util.Comparator;
008import java.util.HashMap;
009import java.util.HashSet;
010import java.util.LinkedList;
011import java.util.List;
012import java.util.Locale;
013import java.util.Map;
014import java.util.Map.Entry;
015import java.util.Set;
016
017import org.openstreetmap.josm.gui.util.ChangeNotifier;
018import org.openstreetmap.josm.plugins.PluginException;
019import org.openstreetmap.josm.plugins.PluginHandler;
020import org.openstreetmap.josm.plugins.PluginInformation;
021import org.openstreetmap.josm.spi.preferences.Config;
022import org.openstreetmap.josm.tools.Logging;
023
024/**
025 * The plugin model behind a {@code PluginListPanel}.
026 */
027public class PluginPreferencesModel extends ChangeNotifier {
028    // remember the initial list of active plugins
029    private final Set<String> currentActivePlugins;
030    private final List<PluginInformation> availablePlugins = new ArrayList<>();
031    private PluginInstallation filterStatus;
032    private String filterExpression;
033    private final List<PluginInformation> displayedPlugins = new ArrayList<>();
034    private final Map<PluginInformation, Boolean> selectedPluginsMap = new HashMap<>();
035    // plugins that still require an update/download
036    private final Set<String> pendingDownloads = new HashSet<>();
037
038    /**
039     * Constructs a new {@code PluginPreferencesModel}.
040     */
041    public PluginPreferencesModel() {
042        currentActivePlugins = new HashSet<>();
043        currentActivePlugins.addAll(Config.getPref().getList("plugins"));
044    }
045
046    /**
047     * Filters the list of displayed plugins by installation status.
048     * @param status The filter used against installation status
049     * @since 13799
050     */
051    public void filterDisplayedPlugins(PluginInstallation status) {
052        filterStatus = status;
053        doFilter();
054    }
055
056    /**
057     * Filters the list of displayed plugins by text.
058     * @param filter The filter used against plugin name, description or version
059     */
060    public void filterDisplayedPlugins(String filter) {
061        filterExpression = filter;
062        doFilter();
063    }
064
065    private void doFilter() {
066        displayedPlugins.clear();
067        for (PluginInformation pi: availablePlugins) {
068            if ((filterStatus == null || matchesInstallationStatus(pi))
069             && (filterExpression == null || pi.matches(filterExpression))) {
070                displayedPlugins.add(pi);
071            }
072        }
073        fireStateChanged();
074    }
075
076    private boolean matchesInstallationStatus(PluginInformation pi) {
077        boolean installed = currentActivePlugins.contains(pi.getName());
078        return PluginInstallation.ALL == filterStatus
079           || (PluginInstallation.INSTALLED == filterStatus && installed)
080           || (PluginInstallation.AVAILABLE == filterStatus && !installed);
081    }
082
083    /**
084     * Sets the list of available plugins.
085     * @param available The available plugins
086     */
087    public void setAvailablePlugins(Collection<PluginInformation> available) {
088        availablePlugins.clear();
089        if (available != null) {
090            availablePlugins.addAll(available);
091        }
092        availablePluginsModified();
093    }
094
095    protected final void availablePluginsModified() {
096        sort();
097        filterDisplayedPlugins(filterStatus);
098        filterDisplayedPlugins(filterExpression);
099        Set<String> activePlugins = new HashSet<>();
100        activePlugins.addAll(Config.getPref().getList("plugins"));
101        for (PluginInformation pi: availablePlugins) {
102            if (selectedPluginsMap.get(pi) == null && activePlugins.contains(pi.name)) {
103                selectedPluginsMap.put(pi, Boolean.TRUE);
104            }
105        }
106        fireStateChanged();
107    }
108
109    protected void updateAvailablePlugin(PluginInformation other) {
110        if (other != null) {
111            PluginInformation pi = getPluginInformation(other.name);
112            if (pi == null) {
113                availablePlugins.add(other);
114                return;
115            }
116            pi.updateFromPluginSite(other);
117        }
118    }
119
120    /**
121     * Updates the list of plugin information objects with new information from
122     * plugin update sites.
123     *
124     * @param fromPluginSite plugin information read from plugin update sites
125     */
126    public void updateAvailablePlugins(Collection<PluginInformation> fromPluginSite) {
127        for (PluginInformation other: fromPluginSite) {
128            updateAvailablePlugin(other);
129        }
130        availablePluginsModified();
131    }
132
133    /**
134     * Replies the list of selected plugin information objects
135     *
136     * @return the list of selected plugin information objects
137     */
138    public List<PluginInformation> getSelectedPlugins() {
139        List<PluginInformation> ret = new LinkedList<>();
140        for (PluginInformation pi: availablePlugins) {
141            if (selectedPluginsMap.get(pi) == null) {
142                continue;
143            }
144            if (selectedPluginsMap.get(pi)) {
145                ret.add(pi);
146            }
147        }
148        return ret;
149    }
150
151    /**
152     * Replies the list of selected plugin information objects
153     *
154     * @return the list of selected plugin information objects
155     */
156    public Set<String> getSelectedPluginNames() {
157        Set<String> ret = new HashSet<>();
158        for (PluginInformation pi: getSelectedPlugins()) {
159            ret.add(pi.name);
160        }
161        return ret;
162    }
163
164    /**
165     * Sorts the list of available plugins
166     */
167    protected void sort() {
168        availablePlugins.sort(Comparator.comparing(
169                o -> o.getName() == null ? "" : o.getName().toLowerCase(Locale.ENGLISH)));
170    }
171
172    /**
173     * Replies the list of plugin informations to display.
174     *
175     * @return the list of plugin informations to display
176     */
177    public List<PluginInformation> getDisplayedPlugins() {
178        return displayedPlugins;
179    }
180
181    /**
182     * Replies the set of plugins waiting for update or download.
183     *
184     * @return the set of plugins waiting for update or download
185     */
186    public Set<PluginInformation> getPluginsScheduledForUpdateOrDownload() {
187        Set<PluginInformation> ret = new HashSet<>();
188        for (String plugin: pendingDownloads) {
189            PluginInformation pi = getPluginInformation(plugin);
190            if (pi == null) {
191                continue;
192            }
193            ret.add(pi);
194        }
195        return ret;
196    }
197
198    /**
199     * Sets whether the plugin is selected or not.
200     *
201     * @param name the name of the plugin
202     * @param selected true, if selected; false, otherwise
203     */
204    public void setPluginSelected(String name, boolean selected) {
205        PluginInformation pi = getPluginInformation(name);
206        if (pi != null) {
207            selectedPluginsMap.put(pi, selected);
208            if (pi.isUpdateRequired()) {
209                pendingDownloads.add(pi.name);
210            }
211        }
212        if (!selected) {
213            pendingDownloads.remove(name);
214        }
215    }
216
217    /**
218     * Removes all the plugin in {@code plugins} from the list of plugins
219     * with a pending download
220     *
221     * @param plugins the list of plugins to clear for a pending download
222     */
223    public void clearPendingPlugins(Collection<PluginInformation> plugins) {
224        if (plugins != null) {
225            for (PluginInformation pi: plugins) {
226                pendingDownloads.remove(pi.name);
227            }
228        }
229    }
230
231    /**
232     * Replies the plugin info with the name <code>name</code>. null, if no
233     * such plugin info exists.
234     *
235     * @param name the name. If null, replies null.
236     * @return the plugin info.
237     */
238    public PluginInformation getPluginInformation(String name) {
239        if (name != null) {
240            for (PluginInformation pi: availablePlugins) {
241                if (name.equals(pi.getName()) || name.equals(pi.provides))
242                    return pi;
243            }
244        }
245        return null;
246    }
247
248    /**
249     * Initializes the model from preferences
250     */
251    public void initFromPreferences() {
252        Collection<String> enabledPlugins = Config.getPref().getList("plugins", null);
253        if (enabledPlugins == null) {
254            this.selectedPluginsMap.clear();
255            return;
256        }
257        for (String name: enabledPlugins) {
258            PluginInformation pi = getPluginInformation(name);
259            if (pi == null) {
260                continue;
261            }
262            setPluginSelected(name, true);
263        }
264    }
265
266    /**
267     * Replies true if the plugin with name <code>name</code> is currently
268     * selected in the plugin model
269     *
270     * @param name the plugin name
271     * @return true if the plugin is selected; false, otherwise
272     */
273    public boolean isSelectedPlugin(String name) {
274        PluginInformation pi = getPluginInformation(name);
275        if (pi == null || selectedPluginsMap.get(pi) == null)
276            return false;
277        return selectedPluginsMap.get(pi);
278    }
279
280    /**
281     * Replies the set of plugins which have been added by the user to
282     * the set of activated plugins.
283     *
284     * @return the set of newly activated plugins
285     */
286    public List<PluginInformation> getNewlyActivatedPlugins() {
287        List<PluginInformation> ret = new LinkedList<>();
288        for (Entry<PluginInformation, Boolean> entry: selectedPluginsMap.entrySet()) {
289            PluginInformation pi = entry.getKey();
290            boolean selected = entry.getValue();
291            if (selected && !currentActivePlugins.contains(pi.name)) {
292                ret.add(pi);
293            }
294        }
295        return ret;
296    }
297
298    /**
299     * Replies the set of plugins which have been removed by the user from
300     * the set of deactivated plugins.
301     *
302     * @return the set of newly deactivated plugins
303     */
304    public List<PluginInformation> getNewlyDeactivatedPlugins() {
305        List<PluginInformation> ret = new LinkedList<>();
306        for (PluginInformation pi: availablePlugins) {
307            if (!currentActivePlugins.contains(pi.name)) {
308                continue;
309            }
310            if (selectedPluginsMap.get(pi) == null || !selectedPluginsMap.get(pi)) {
311                ret.add(pi);
312            }
313        }
314        return ret;
315    }
316
317    /**
318     * Replies the set of all available plugins.
319     *
320     * @return the set of all available plugins
321     */
322    public List<PluginInformation> getAvailablePlugins() {
323        return new LinkedList<>(availablePlugins);
324    }
325
326    /**
327     * Replies the set of plugin names which have been added by the user to
328     * the set of activated plugins.
329     *
330     * @return the set of newly activated plugin names
331     */
332    public Set<String> getNewlyActivatedPluginNames() {
333        Set<String> ret = new HashSet<>();
334        List<PluginInformation> plugins = getNewlyActivatedPlugins();
335        for (PluginInformation pi: plugins) {
336            ret.add(pi.name);
337        }
338        return ret;
339    }
340
341    /**
342     * Replies true if the set of active plugins has been changed by the user
343     * in this preference model. He has either added plugins or removed plugins
344     * being active before.
345     *
346     * @return true if the collection of active plugins has changed
347     */
348    public boolean isActivePluginsChanged() {
349        Set<String> newActivePlugins = getSelectedPluginNames();
350        return !newActivePlugins.equals(currentActivePlugins);
351    }
352
353    /**
354     * Refreshes the local version field on the plugins in <code>plugins</code> with
355     * the version in the manifest of the downloaded "jar.new"-file for this plugin.
356     *
357     * @param plugins the collections of plugins to refresh
358     */
359    public void refreshLocalPluginVersion(Collection<PluginInformation> plugins) {
360        if (plugins != null) {
361            for (PluginInformation pi : plugins) {
362                File downloadedPluginFile = PluginHandler.findUpdatedJar(pi.name);
363                if (downloadedPluginFile == null) {
364                    continue;
365                }
366                try {
367                    PluginInformation newinfo = new PluginInformation(downloadedPluginFile, pi.name);
368                    PluginInformation oldinfo = getPluginInformation(pi.name);
369                    if (oldinfo != null) {
370                        oldinfo.updateLocalInfo(newinfo);
371                    }
372                } catch (PluginException e) {
373                    Logging.error(e);
374                }
375            }
376        }
377    }
378}