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