// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.osm;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
import org.openstreetmap.josm.gui.JosmUserIdentityManager;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.tools.SubclassFilteredCollection;
/**
* ChangesetCache is global in-memory cache for changesets downloaded from
* an OSM API server. The unique instance is available as singleton, see
* {@link #getInstance()}.
*
* Clients interested in cache updates can register for {@link ChangesetCacheEvent}s
* using {@link #addChangesetCacheListener(ChangesetCacheListener)}. They can use
* {@link #removeChangesetCacheListener(ChangesetCacheListener)} to unregister as
* cache event listener.
*
* The cache itself listens to {@link java.util.prefs.PreferenceChangeEvent}s. It
* clears itself if the OSM API URL is changed in the preferences.
*
* {@link ChangesetCacheEvent}s are delivered on the EDT.
*
*/
public final class ChangesetCache implements PreferenceChangedListener {
/** the unique instance */
private static final ChangesetCache instance = new ChangesetCache();
/** the cached changesets */
private final Map<Integer, Changeset> cache = new HashMap<>();
private final CopyOnWriteArrayList<ChangesetCacheListener> listeners = new CopyOnWriteArrayList<>();
/**
* Constructs a new {@code ChangesetCache}.
*/
private ChangesetCache() {
Main.pref.addPreferenceChangeListener(this);
}
/**
* Replies the unique instance of the cache
* @return the unique instance of the cache
*/
public static ChangesetCache getInstance() {
return instance;
}
/**
* Add a changeset cache listener.
* @param listener changeset cache listener to add
*/
public void addChangesetCacheListener(ChangesetCacheListener listener) {
if (listener != null) {
listeners.addIfAbsent(listener);
}
}
/**
* Remove a changeset cache listener.
* @param listener changeset cache listener to remove
*/
public void removeChangesetCacheListener(ChangesetCacheListener listener) {
if (listener != null) {
listeners.remove(listener);
}
}
private void fireChangesetCacheEvent(final ChangesetCacheEvent e) {
GuiHelper.runInEDT(() -> {
for (ChangesetCacheListener l: listeners) {
l.changesetCacheUpdated(e);
}
});
}
private void update(Changeset cs, DefaultChangesetCacheEvent e) {
if (cs == null) return;
if (cs.isNew()) return;
Changeset inCache = cache.get(cs.getId());
if (inCache != null) {
inCache.mergeFrom(cs);
e.rememberUpdatedChangeset(inCache);
} else {
e.rememberAddedChangeset(cs);
cache.put(cs.getId(), cs);
}
}
/**
* Update a single changeset.
* @param cs changeset to update
*/
public void update(Changeset cs) {
DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
update(cs, e);
fireChangesetCacheEvent(e);
}
/**
* Update a collection of changesets.
* @param changesets changesets to update
*/
public void update(Collection<Changeset> changesets) {
if (changesets == null || changesets.isEmpty()) return;
DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
for (Changeset cs: changesets) {
update(cs, e);
}
fireChangesetCacheEvent(e);
}
/**
* Determines if the cache contains an entry for given changeset identifier.
* @param id changeset id
* @return {@code true} if the cache contains an entry for {@code id}
*/
public boolean contains(int id) {
if (id <= 0) return false;
return cache.get(id) != null;
}
/**
* Determines if the cache contains an entry for given changeset.
* @param cs changeset
* @return {@code true} if the cache contains an entry for {@code cs}
*/
public boolean contains(Changeset cs) {
if (cs == null) return false;
if (cs.isNew()) return false;
return contains(cs.getId());
}
/**
* Returns the entry for given changeset identifier.
* @param id changeset id
* @return the entry for given changeset identifier, or null
*/
public Changeset get(int id) {
return cache.get(id);
}
/**
* Returns the list of changesets contained in the cache.
* @return the list of changesets contained in the cache
*/
public Set<Changeset> getChangesets() {
return new HashSet<>(cache.values());
}
private void remove(int id, DefaultChangesetCacheEvent e) {
if (id <= 0) return;
Changeset cs = cache.get(id);
if (cs == null) return;
cache.remove(id);
e.rememberRemovedChangeset(cs);
}
/**
* Remove the entry for the given changeset identifier.
* A {@link ChangesetCacheEvent} is fired.
* @param id changeset id
*/
public void remove(int id) {
DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
remove(id, e);
if (!e.isEmpty()) {
fireChangesetCacheEvent(e);
}
}
/**
* Remove the entry for the given changeset.
* A {@link ChangesetCacheEvent} is fired.
* @param cs changeset
*/
public void remove(Changeset cs) {
if (cs == null) return;
if (cs.isNew()) return;
remove(cs.getId());
}
/**
* Removes the changesets in <code>changesets</code> from the cache.
* A {@link ChangesetCacheEvent} is fired.
*
* @param changesets the changesets to remove. Ignored if null.
*/
public void remove(Collection<Changeset> changesets) {
if (changesets == null) return;
DefaultChangesetCacheEvent evt = new DefaultChangesetCacheEvent(this);
for (Changeset cs : changesets) {
if (cs == null || cs.isNew()) {
continue;
}
remove(cs.getId(), evt);
}
if (!evt.isEmpty()) {
fireChangesetCacheEvent(evt);
}
}
/**
* Returns the number of changesets contained in the cache.
* @return the number of changesets contained in the cache
*/
public int size() {
return cache.size();
}
/**
* Clears the cache.
*/
public void clear() {
DefaultChangesetCacheEvent e = new DefaultChangesetCacheEvent(this);
for (Changeset cs: cache.values()) {
e.rememberRemovedChangeset(cs);
}
cache.clear();
fireChangesetCacheEvent(e);
}
/**
* Replies the list of open changesets.
* @return The list of open changesets
*/
public List<Changeset> getOpenChangesets() {
return cache.values().stream()
.filter(Changeset::isOpen)
.collect(Collectors.toList());
}
/**
* If the current user {@link JosmUserIdentityManager#isAnonymous() is known}, the {@link #getOpenChangesets() open changesets}
* for the {@link JosmUserIdentityManager#isCurrentUser(User) current user} are returned. Otherwise,
* the unfiltered {@link #getOpenChangesets() open changesets} are returned.
*
* @return a list of changesets
*/
public List<Changeset> getOpenChangesetsForCurrentUser() {
if (JosmUserIdentityManager.getInstance().isAnonymous()) {
return getOpenChangesets();
} else {
return new ArrayList<>(SubclassFilteredCollection.filter(getOpenChangesets(),
object -> JosmUserIdentityManager.getInstance().isCurrentUser(object.getUser())));
}
}
/* ------------------------------------------------------------------------- */
/* interface PreferenceChangedListener */
/* ------------------------------------------------------------------------- */
@Override
public void preferenceChanged(PreferenceChangeEvent e) {
if (e.getKey() == null || !"osm-server.url".equals(e.getKey()))
return;
// clear the cache when the API url changes
if (e.getOldValue() == null || e.getNewValue() == null || !e.getOldValue().equals(e.getNewValue())) {
clear();
}
}
}