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