001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.ArrayList; 007import java.util.Collection; 008import java.util.HashMap; 009import java.util.LinkedHashSet; 010import java.util.List; 011import java.util.Map; 012import java.util.Objects; 013 014/** 015 * A simple class to keep a list of user names. 016 * 017 * Instead of storing user names as strings with every OSM primitive, we store 018 * a reference to an user object, and make sure that for each username there 019 * is only one user object. 020 * 021 * @since 227 022 */ 023public final class User { 024 025 private static long uidCounter; 026 027 /** 028 * the map of known users 029 */ 030 private static Map<Long, User> userMap = new HashMap<>(); 031 032 /** 033 * The anonymous user is a local user used in places where no user is known. 034 * @see #getAnonymous() 035 */ 036 private static final User ANONYMOUS = createLocalUser(tr("<anonymous>")); 037 038 private static long getNextLocalUid() { 039 uidCounter--; 040 return uidCounter; 041 } 042 043 /** 044 * Creates a local user with the given name 045 * 046 * @param name the name 047 * @return a new local user with the given name 048 */ 049 public static synchronized User createLocalUser(String name) { 050 for (long i = -1; i >= uidCounter; --i) { 051 User olduser = getById(i); 052 if (olduser != null && olduser.hasName(name)) 053 return olduser; 054 } 055 User user = new User(getNextLocalUid(), name); 056 userMap.put(user.getId(), user); 057 return user; 058 } 059 060 private static User lastUser; 061 062 /** 063 * Creates a user known to the OSM server 064 * 065 * @param uid the user id 066 * @param name the name 067 * @return a new OSM user with the given name and uid 068 */ 069 public static synchronized User createOsmUser(long uid, String name) { 070 071 if (lastUser != null && lastUser.getId() == uid) { 072 if (name != null) { 073 lastUser.setPreferredName(name); 074 } 075 return lastUser; 076 } 077 078 User user = userMap.computeIfAbsent(uid, k -> new User(uid, name)); 079 if (name != null) user.addName(name); 080 081 lastUser = user; 082 083 return user; 084 } 085 086 /** 087 * clears the static map of user ids to user objects 088 */ 089 public static synchronized void clearUserMap() { 090 userMap.clear(); 091 lastUser = null; 092 } 093 094 /** 095 * Returns the user with user id <code>uid</code> or null if this user doesn't exist 096 * 097 * @param uid the user id 098 * @return the user; null, if there is no user with this id 099 */ 100 public static synchronized User getById(long uid) { 101 return userMap.get(uid); 102 } 103 104 /** 105 * Returns the list of users with name <code>name</code> or the empty list if 106 * no such users exist 107 * 108 * @param name the user name 109 * @return the list of users with name <code>name</code> or the empty list if 110 * no such users exist 111 */ 112 public static synchronized List<User> getByName(String name) { 113 if (name == null) { 114 name = ""; 115 } 116 List<User> ret = new ArrayList<>(); 117 for (User user: userMap.values()) { 118 if (user.hasName(name)) { 119 ret.add(user); 120 } 121 } 122 return ret; 123 } 124 125 /** 126 * Replies the anonymous user 127 * @return The anonymous user 128 */ 129 public static User getAnonymous() { 130 return ANONYMOUS; 131 } 132 133 /** the user name */ 134 private final LinkedHashSet<String> names = new LinkedHashSet<>(); 135 /** the user id */ 136 private final long uid; 137 138 /** 139 * Replies the user name 140 * 141 * @return the user name. Never <code>null</code>, but may be the empty string 142 * @see #getByName(String) 143 * @see #createOsmUser(long, String) 144 * @see #createLocalUser(String) 145 */ 146 public String getName() { 147 return names.isEmpty() ? "" : names.iterator().next(); 148 } 149 150 /** 151 * Returns the list of user names 152 * 153 * @return list of names 154 */ 155 public List<String> getNames() { 156 return new ArrayList<>(names); 157 } 158 159 /** 160 * Adds a user name to the list if it is not there, yet. 161 * 162 * @param name User name 163 * @throws NullPointerException if name is null 164 */ 165 public void addName(String name) { 166 names.add(Objects.requireNonNull(name, "name")); 167 } 168 169 /** 170 * Sets the preferred user name, i.e., the one that will be returned when calling {@link #getName()}. 171 * 172 * Rationale: A user can change its name multiple times and after reading various (outdated w.r.t. user name) 173 * data files it is unclear which is the up-to-date user name. 174 * @param name the preferred user name to set 175 * @throws NullPointerException if name is null 176 */ 177 public void setPreferredName(String name) { 178 if (names.size() == 1 && names.contains(name)) { 179 return; 180 } 181 final Collection<String> allNames = new LinkedHashSet<>(names); 182 names.clear(); 183 names.add(Objects.requireNonNull(name, "name")); 184 names.addAll(allNames); 185 } 186 187 /** 188 * Returns true if the name is in the names list 189 * 190 * @param name User name 191 * @return <code>true</code> if the name is in the names list 192 */ 193 public boolean hasName(String name) { 194 return names.contains(name); 195 } 196 197 /** 198 * Replies the user id. If this user is known to the OSM server the positive user id 199 * from the server is replied. Otherwise, a negative local value is replied. 200 * 201 * A negative local is only unique during an editing session. It is lost when the 202 * application is closed and there is no guarantee that a negative local user id is 203 * always bound to a user with the same name. 204 * 205 * @return the user id 206 */ 207 public long getId() { 208 return uid; 209 } 210 211 /** 212 * Private constructor, only called from get method. 213 * @param uid user id 214 * @param name user name 215 */ 216 private User(long uid, String name) { 217 this.uid = uid; 218 if (name != null) { 219 addName(name); 220 } 221 } 222 223 /** 224 * Determines if this user is known to OSM 225 * @return {@code true} if this user is known to OSM, {@code false} otherwise 226 */ 227 public boolean isOsmUser() { 228 return uid > 0; 229 } 230 231 /** 232 * Determines if this user is local 233 * @return {@code true} if this user is local, {@code false} otherwise 234 */ 235 public boolean isLocalUser() { 236 return uid < 0; 237 } 238 239 @Override 240 public int hashCode() { 241 return Objects.hash(uid); 242 } 243 244 @Override 245 public boolean equals(Object obj) { 246 if (this == obj) return true; 247 if (obj == null || getClass() != obj.getClass()) return false; 248 User user = (User) obj; 249 return uid == user.uid; 250 } 251 252 @Override 253 public String toString() { 254 StringBuilder s = new StringBuilder(); 255 s.append("id:").append(uid); 256 if (names.size() == 1) { 257 s.append(" name:").append(getName()); 258 } else if (names.size() > 1) { 259 s.append(String.format(" %d names:%s", names.size(), getName())); 260 } 261 return s.toString(); 262 } 263}