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.data.UserIdentityManager; 015import org.openstreetmap.josm.spi.preferences.Config; 016import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 017import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 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}