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