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}