001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.HashMap;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Map;
010import java.util.Set;
011import java.util.concurrent.CopyOnWriteArrayList;
012import java.util.stream.Collectors;
013
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
016import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
017import org.openstreetmap.josm.gui.JosmUserIdentityManager;
018import org.openstreetmap.josm.gui.util.GuiHelper;
019import org.openstreetmap.josm.tools.SubclassFilteredCollection;
020
021/**
022 * ChangesetCache is global in-memory cache for changesets downloaded from
023 * an OSM API server. The unique instance is available as singleton, see
024 * {@link #getInstance()}.
025 *
026 * Clients interested in cache updates can register for {@link ChangesetCacheEvent}s
027 * using {@link #addChangesetCacheListener(ChangesetCacheListener)}. They can use
028 * {@link #removeChangesetCacheListener(ChangesetCacheListener)} to unregister as
029 * cache event listener.
030 *
031 * The cache itself listens to {@link java.util.prefs.PreferenceChangeEvent}s. It
032 * clears itself if the OSM API URL is changed in the preferences.
033 *
034 * {@link ChangesetCacheEvent}s are delivered on the EDT.
035 *
036 */
037public final class ChangesetCache implements PreferenceChangedListener {
038    /** the unique instance */
039    private static final ChangesetCache instance = new ChangesetCache();
040
041    /**
042     * Replies the unique instance of the cache
043     *
044     * @return the unique instance of the cache
045     */
046    public static ChangesetCache getInstance() {
047        return instance;
048    }
049
050    /** the cached changesets */
051    private final Map<Integer, Changeset> cache = new HashMap<>();
052
053    private final CopyOnWriteArrayList<ChangesetCacheListener> listeners = new CopyOnWriteArrayList<>();
054
055    private ChangesetCache() {
056        Main.pref.addPreferenceChangeListener(this);
057    }
058
059    public void addChangesetCacheListener(ChangesetCacheListener listener) {
060        if (listener != null) {
061            listeners.addIfAbsent(listener);
062        }
063    }
064
065    public void removeChangesetCacheListener(ChangesetCacheListener listener) {
066        if (listener != null) {
067            listeners.remove(listener);
068        }
069    }
070
071    private void fireChangesetCacheEvent(final ChangesetCacheEvent e) {
072        GuiHelper.runInEDT(() -> {
073            for (ChangesetCacheListener l: listeners) {
074                l.changesetCacheUpdated(e);
075            }
076        });
077    }
078
079    private void update(Changeset cs, DefaultChangesetCacheEvent e) {
080        if (cs == null) return;
081        if (cs.isNew()) return;
082        Changeset inCache = cache.get(cs.getId());
083        if (inCache != null) {
084            inCache.mergeFrom(cs);
085            e.rememberUpdatedChangeset(inCache);
086        } else {
087            e.rememberAddedChangeset(cs);
088            cache.put(cs.getId(), cs);
089        }
090    }
091
092    public void update(Changeset cs) {
093        DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
094        update(cs, e);
095        fireChangesetCacheEvent(e);
096    }
097
098    public void update(Collection<Changeset> changesets) {
099        if (changesets == null || changesets.isEmpty()) return;
100        DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
101        for (Changeset cs: changesets) {
102            update(cs, e);
103        }
104        fireChangesetCacheEvent(e);
105    }
106
107    public boolean contains(int id) {
108        if (id <= 0) return false;
109        return cache.get(id) != null;
110    }
111
112    public boolean contains(Changeset cs) {
113        if (cs == null) return false;
114        if (cs.isNew()) return false;
115        return contains(cs.getId());
116    }
117
118    public Changeset get(int id) {
119        return cache.get(id);
120    }
121
122    public Set<Changeset> getChangesets() {
123        return new HashSet<>(cache.values());
124    }
125
126    private void remove(int id, DefaultChangesetCacheEvent e) {
127        if (id <= 0) return;
128        Changeset cs = cache.get(id);
129        if (cs == null) return;
130        cache.remove(id);
131        e.rememberRemovedChangeset(cs);
132    }
133
134    public void remove(int id) {
135        DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
136        remove(id, e);
137        if (!e.isEmpty()) {
138            fireChangesetCacheEvent(e);
139        }
140    }
141
142    public void remove(Changeset cs) {
143        if (cs == null) return;
144        if (cs.isNew()) return;
145        remove(cs.getId());
146    }
147
148    /**
149     * Removes the changesets in <code>changesets</code> from the cache. A
150     * {@link ChangesetCacheEvent} is fired.
151     *
152     * @param changesets the changesets to remove. Ignored if null.
153     */
154    public void remove(Collection<Changeset> changesets) {
155        if (changesets == null) return;
156        DefaultChangesetCacheEvent evt = new DefaultChangesetCacheEvent(this);
157        for (Changeset cs : changesets) {
158            if (cs == null || cs.isNew()) {
159                continue;
160            }
161            remove(cs.getId(), evt);
162        }
163        if (!evt.isEmpty()) {
164            fireChangesetCacheEvent(evt);
165        }
166    }
167
168    public int size() {
169        return cache.size();
170    }
171
172    public void clear() {
173        DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
174        for (Changeset cs: cache.values()) {
175            e.rememberRemovedChangeset(cs);
176        }
177        cache.clear();
178        fireChangesetCacheEvent(e);
179    }
180
181    /**
182     * Replies the list of open changesets.
183     * @return The list of open changesets
184     */
185    public List<Changeset> getOpenChangesets() {
186        return cache.values().stream()
187                .filter(Changeset::isOpen)
188                .collect(Collectors.toList());
189    }
190
191    /**
192     * If the current user {@link JosmUserIdentityManager#isAnonymous() is known}, the {@link #getOpenChangesets() open changesets}
193     * for the {@link JosmUserIdentityManager#isCurrentUser(User) current user} are returned. Otherwise,
194     * the unfiltered {@link #getOpenChangesets() open changesets} are returned.
195     *
196     * @return a list of changesets
197     */
198    public List<Changeset> getOpenChangesetsForCurrentUser() {
199        if (JosmUserIdentityManager.getInstance().isAnonymous()) {
200            return getOpenChangesets();
201        } else {
202            return new ArrayList<>(SubclassFilteredCollection.filter(getOpenChangesets(),
203                    object -> JosmUserIdentityManager.getInstance().isCurrentUser(object.getUser())));
204        }
205    }
206
207    /* ------------------------------------------------------------------------- */
208    /* interface PreferenceChangedListener                                       */
209    /* ------------------------------------------------------------------------- */
210    @Override
211    public void preferenceChanged(PreferenceChangeEvent e) {
212        if (e.getKey() == null || !"osm-server.url".equals(e.getKey()))
213            return;
214
215        // clear the cache when the API url changes
216        if (e.getOldValue() == null || e.getNewValue() == null || !e.getOldValue().equals(e.getNewValue())) {
217            clear();
218        }
219    }
220}