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    /** the cached changesets */
042    private final Map<Integer, Changeset> cache = new HashMap<>();
043
044    private final CopyOnWriteArrayList<ChangesetCacheListener> listeners = new CopyOnWriteArrayList<>();
045
046    /**
047     * Constructs a new {@code ChangesetCache}.
048     */
049    private ChangesetCache() {
050        Main.pref.addPreferenceChangeListener(this);
051    }
052
053    /**
054     * Replies the unique instance of the cache
055     * @return the unique instance of the cache
056     */
057    public static ChangesetCache getInstance() {
058        return instance;
059    }
060
061    /**
062     * Add a changeset cache listener.
063     * @param listener changeset cache listener to add
064     */
065    public void addChangesetCacheListener(ChangesetCacheListener listener) {
066        if (listener != null) {
067            listeners.addIfAbsent(listener);
068        }
069    }
070
071    /**
072     * Remove a changeset cache listener.
073     * @param listener changeset cache listener to remove
074     */
075    public void removeChangesetCacheListener(ChangesetCacheListener listener) {
076        if (listener != null) {
077            listeners.remove(listener);
078        }
079    }
080
081    private void fireChangesetCacheEvent(final ChangesetCacheEvent e) {
082        GuiHelper.runInEDT(() -> {
083            for (ChangesetCacheListener l: listeners) {
084                l.changesetCacheUpdated(e);
085            }
086        });
087    }
088
089    private void update(Changeset cs, DefaultChangesetCacheEvent e) {
090        if (cs == null) return;
091        if (cs.isNew()) return;
092        Changeset inCache = cache.get(cs.getId());
093        if (inCache != null) {
094            inCache.mergeFrom(cs);
095            e.rememberUpdatedChangeset(inCache);
096        } else {
097            e.rememberAddedChangeset(cs);
098            cache.put(cs.getId(), cs);
099        }
100    }
101
102    /**
103     * Update a single changeset.
104     * @param cs changeset to update
105     */
106    public void update(Changeset cs) {
107        DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
108        update(cs, e);
109        fireChangesetCacheEvent(e);
110    }
111
112    /**
113     * Update a collection of changesets.
114     * @param changesets changesets to update
115     */
116    public void update(Collection<Changeset> changesets) {
117        if (changesets == null || changesets.isEmpty()) return;
118        DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
119        for (Changeset cs: changesets) {
120            update(cs, e);
121        }
122        fireChangesetCacheEvent(e);
123    }
124
125    /**
126     * Determines if the cache contains an entry for given changeset identifier.
127     * @param id changeset id
128     * @return {@code true} if the cache contains an entry for {@code id}
129     */
130    public boolean contains(int id) {
131        if (id <= 0) return false;
132        return cache.get(id) != null;
133    }
134
135    /**
136     * Determines if the cache contains an entry for given changeset.
137     * @param cs changeset
138     * @return {@code true} if the cache contains an entry for {@code cs}
139     */
140    public boolean contains(Changeset cs) {
141        if (cs == null) return false;
142        if (cs.isNew()) return false;
143        return contains(cs.getId());
144    }
145
146    /**
147     * Returns the entry for given changeset identifier.
148     * @param id changeset id
149     * @return the entry for given changeset identifier, or null
150     */
151    public Changeset get(int id) {
152        return cache.get(id);
153    }
154
155    /**
156     * Returns the list of changesets contained in the cache.
157     * @return the list of changesets contained in the cache
158     */
159    public Set<Changeset> getChangesets() {
160        return new HashSet<>(cache.values());
161    }
162
163    private void remove(int id, DefaultChangesetCacheEvent e) {
164        if (id <= 0) return;
165        Changeset cs = cache.get(id);
166        if (cs == null) return;
167        cache.remove(id);
168        e.rememberRemovedChangeset(cs);
169    }
170
171    /**
172     * Remove the entry for the given changeset identifier.
173     * A {@link ChangesetCacheEvent} is fired.
174     * @param id changeset id
175     */
176    public void remove(int id) {
177        DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
178        remove(id, e);
179        if (!e.isEmpty()) {
180            fireChangesetCacheEvent(e);
181        }
182    }
183
184    /**
185     * Remove the entry for the given changeset.
186     * A {@link ChangesetCacheEvent} is fired.
187     * @param cs changeset
188     */
189    public void remove(Changeset cs) {
190        if (cs == null) return;
191        if (cs.isNew()) return;
192        remove(cs.getId());
193    }
194
195    /**
196     * Removes the changesets in <code>changesets</code> from the cache.
197     * A {@link ChangesetCacheEvent} is fired.
198     *
199     * @param changesets the changesets to remove. Ignored if null.
200     */
201    public void remove(Collection<Changeset> changesets) {
202        if (changesets == null) return;
203        DefaultChangesetCacheEvent evt = new DefaultChangesetCacheEvent(this);
204        for (Changeset cs : changesets) {
205            if (cs == null || cs.isNew()) {
206                continue;
207            }
208            remove(cs.getId(), evt);
209        }
210        if (!evt.isEmpty()) {
211            fireChangesetCacheEvent(evt);
212        }
213    }
214
215    /**
216     * Returns the number of changesets contained in the cache.
217     * @return the number of changesets contained in the cache
218     */
219    public int size() {
220        return cache.size();
221    }
222
223    /**
224     * Clears the cache.
225     */
226    public void clear() {
227        DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
228        for (Changeset cs: cache.values()) {
229            e.rememberRemovedChangeset(cs);
230        }
231        cache.clear();
232        fireChangesetCacheEvent(e);
233    }
234
235    /**
236     * Replies the list of open changesets.
237     * @return The list of open changesets
238     */
239    public List<Changeset> getOpenChangesets() {
240        return cache.values().stream()
241                .filter(Changeset::isOpen)
242                .collect(Collectors.toList());
243    }
244
245    /**
246     * If the current user {@link JosmUserIdentityManager#isAnonymous() is known}, the {@link #getOpenChangesets() open changesets}
247     * for the {@link JosmUserIdentityManager#isCurrentUser(User) current user} are returned. Otherwise,
248     * the unfiltered {@link #getOpenChangesets() open changesets} are returned.
249     *
250     * @return a list of changesets
251     */
252    public List<Changeset> getOpenChangesetsForCurrentUser() {
253        if (JosmUserIdentityManager.getInstance().isAnonymous()) {
254            return getOpenChangesets();
255        } else {
256            return new ArrayList<>(SubclassFilteredCollection.filter(getOpenChangesets(),
257                    object -> JosmUserIdentityManager.getInstance().isCurrentUser(object.getUser())));
258        }
259    }
260
261    /* ------------------------------------------------------------------------- */
262    /* interface PreferenceChangedListener                                       */
263    /* ------------------------------------------------------------------------- */
264    @Override
265    public void preferenceChanged(PreferenceChangeEvent e) {
266        if (e.getKey() == null || !"osm-server.url".equals(e.getKey()))
267            return;
268
269        // clear the cache when the API url changes
270        if (e.getOldValue() == null || e.getNewValue() == null || !e.getOldValue().equals(e.getNewValue())) {
271            clear();
272        }
273    }
274}