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}