001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.text.MessageFormat; 008 009import org.openstreetmap.josm.Main; 010import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 011import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 012import org.openstreetmap.josm.data.Preferences.StringSetting; 013import org.openstreetmap.josm.data.osm.UserInfo; 014import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder; 015import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 016import org.openstreetmap.josm.io.OnlineResource; 017import org.openstreetmap.josm.io.OsmApi; 018import org.openstreetmap.josm.io.OsmServerUserInfoReader; 019import org.openstreetmap.josm.io.OsmTransferException; 020import org.openstreetmap.josm.io.auth.CredentialsManager; 021import org.openstreetmap.josm.tools.CheckParameterUtil; 022 023/** 024 * JosmUserIdentityManager is a global object which keeps track of what JOSM knows about 025 * the identity of the current user. 026 * 027 * JOSM can be operated anonymously provided the current user never invokes an operation 028 * on the OSM server which required authentication. In this case JOSM neither knows 029 * the user name of the OSM account of the current user nor its unique id. Perhaps the 030 * user doesn't have one. 031 * 032 * If the current user supplies a user name and a password in the JOSM preferences JOSM 033 * can partially identify the user. 034 * 035 * The current user is fully identified if JOSM knows both the user name and the unique 036 * id of the users OSM account. The latter is retrieved from the OSM server with a 037 * <tt>GET /api/0.6/user/details</tt> request, submitted with the user name and password 038 * of the current user. 039 * 040 * The global JosmUserIdentityManager listens to {@link PreferenceChangeEvent}s and keeps track 041 * of what the current JOSM instance knows about the current user. Other subsystems can 042 * let the global JosmUserIdentityManager know in case they fully identify the current user, see 043 * {@link #setFullyIdentified}. 044 * 045 * The information kept by the JosmUserIdentityManager can be used to 046 * <ul> 047 * <li>safely query changesets owned by the current user based on its user id, not on its user name</li> 048 * <li>safely search for objects last touched by the current user based on its user id, not on its user name</li> 049 * </ul> 050 * 051 */ 052public final class JosmUserIdentityManager implements PreferenceChangedListener{ 053 054 private static JosmUserIdentityManager instance; 055 056 /** 057 * Replies the unique instance of the JOSM user identity manager 058 * 059 * @return the unique instance of the JOSM user identity manager 060 */ 061 public static JosmUserIdentityManager getInstance() { 062 if (instance == null) { 063 instance = new JosmUserIdentityManager(); 064 if (OsmApi.isUsingOAuth() && OAuthAccessTokenHolder.getInstance().containsAccessToken() && 065 !Main.isOffline(OnlineResource.OSM_API)) { 066 try { 067 instance.initFromOAuth(Main.parent); 068 } catch (Exception e) { 069 Main.error(e); 070 // Fall back to preferences if OAuth identification fails for any reason 071 instance.initFromPreferences(); 072 } 073 } else { 074 instance.initFromPreferences(); 075 } 076 Main.pref.addPreferenceChangeListener(instance); 077 } 078 return instance; 079 } 080 081 private String userName; 082 private UserInfo userInfo; 083 private boolean accessTokenKeyChanged; 084 private boolean accessTokenSecretChanged; 085 086 private JosmUserIdentityManager() { 087 } 088 089 /** 090 * Remembers the fact that the current JOSM user is anonymous. 091 */ 092 public void setAnonymous() { 093 userName = null; 094 userInfo = null; 095 } 096 097 /** 098 * Remebers the fact that the current JOSM user is partially identified 099 * by the user name of its OSM account. 100 * 101 * @param userName the user name. Must not be null. Must not be empty (whitespace only). 102 * @throws IllegalArgumentException thrown if userName is null 103 * @throws IllegalArgumentException thrown if userName is empty 104 */ 105 public void setPartiallyIdentified(String userName) throws IllegalArgumentException { 106 CheckParameterUtil.ensureParameterNotNull(userName, "userName"); 107 if (userName.trim().isEmpty()) 108 throw new IllegalArgumentException(MessageFormat.format("Expected non-empty value for parameter ''{0}'', got ''{1}''", "userName", userName)); 109 this.userName = userName; 110 userInfo = null; 111 } 112 113 /** 114 * Remembers the fact that the current JOSM user is fully identified with a 115 * verified pair of user name and user id. 116 * 117 * @param username the user name. Must not be null. Must not be empty. 118 * @param userinfo additional information about the user, retrieved from the OSM server and including the user id 119 * @throws IllegalArgumentException thrown if userName is null 120 * @throws IllegalArgumentException thrown if userName is empty 121 * @throws IllegalArgumentException thrown if userinfo is null 122 */ 123 public void setFullyIdentified(String username, UserInfo userinfo) throws IllegalArgumentException { 124 CheckParameterUtil.ensureParameterNotNull(username, "username"); 125 if (username.trim().isEmpty()) 126 throw new IllegalArgumentException(tr("Expected non-empty value for parameter ''{0}'', got ''{1}''", "userName", userName)); 127 CheckParameterUtil.ensureParameterNotNull(userinfo, "userinfo"); 128 this.userName = username; 129 this.userInfo = userinfo; 130 } 131 132 /** 133 * Replies true if the current JOSM user is anonymous. 134 * 135 * @return {@code true} if the current user is anonymous. 136 */ 137 public boolean isAnonymous() { 138 return userName == null && userInfo == null; 139 } 140 141 /** 142 * Replies true if the current JOSM user is partially identified. 143 * 144 * @return true if the current JOSM user is partially identified. 145 */ 146 public boolean isPartiallyIdentified() { 147 return userName != null && userInfo == null; 148 } 149 150 /** 151 * Replies true if the current JOSM user is fully identified. 152 * 153 * @return true if the current JOSM user is fully identified. 154 */ 155 public boolean isFullyIdentified() { 156 return userName != null && userInfo != null; 157 } 158 159 /** 160 * Replies the user name of the current JOSM user. null, if {@link #isAnonymous()} is true. 161 * 162 * @return the user name of the current JOSM user 163 */ 164 public String getUserName() { 165 return userName; 166 } 167 168 /** 169 * Replies the user id of the current JOSM user. 0, if {@link #isAnonymous()} or 170 * {@link #isPartiallyIdentified()} is true. 171 * 172 * @return the user id of the current JOSM user 173 */ 174 public int getUserId() { 175 if (userInfo == null) return 0; 176 return userInfo.getId(); 177 } 178 179 /** 180 * Replies verified additional information about the current user if the user is 181 * {@link #isFullyIdentified()}. 182 * 183 * @return verified additional information about the current user 184 */ 185 public UserInfo getUserInfo() { 186 return userInfo; 187 } 188 189 /** 190 * Initializes the user identity manager from Basic Authentication values in the {@link org.openstreetmap.josm.data.Preferences} 191 * This method should be called if {@code osm-server.auth-method} is set to {@code basic}. 192 * @see #initFromOAuth 193 */ 194 public void initFromPreferences() { 195 String userName = CredentialsManager.getInstance().getUsername(); 196 if (isAnonymous()) { 197 if (userName != null && !userName.trim().isEmpty()) { 198 setPartiallyIdentified(userName); 199 } 200 } else { 201 if (userName != null && !userName.equals(this.userName)) { 202 setPartiallyIdentified(userName); 203 } else { 204 // same name in the preferences as JOSM already knows about. 205 // keep the state, be it partially or fully identified 206 } 207 } 208 } 209 210 /** 211 * Initializes the user identity manager from OAuth request of user details. 212 * This method should be called if {@code osm-server.auth-method} is set to {@code oauth}. 213 * @param parent component relative to which the {@link PleaseWaitDialog} is displayed. 214 * @see #initFromPreferences 215 * @since 5434 216 */ 217 public void initFromOAuth(Component parent) { 218 try { 219 UserInfo info = new OsmServerUserInfoReader().fetchUserInfo(NullProgressMonitor.INSTANCE); 220 setFullyIdentified(info.getDisplayName(), info); 221 } catch (IllegalArgumentException | OsmTransferException e) { 222 Main.error(e); 223 } 224 } 225 226 /** 227 * Replies true if the user with name <code>username</code> is the current 228 * user 229 * 230 * @param username the user name 231 * @return true if the user with name <code>username</code> is the current 232 * user 233 */ 234 public boolean isCurrentUser(String username) { 235 if (username == null || this.userName == null) return false; 236 return this.userName.equals(username); 237 } 238 239 /* ------------------------------------------------------------------- */ 240 /* interface PreferenceChangeListener */ 241 /* ------------------------------------------------------------------- */ 242 @Override 243 public void preferenceChanged(PreferenceChangeEvent evt) { 244 switch (evt.getKey()) { 245 case "osm-server.username": 246 String newUserName = null; 247 if (evt.getNewValue() instanceof StringSetting) { 248 newUserName = ((StringSetting) evt.getNewValue()).getValue(); 249 } 250 if (newUserName == null || newUserName.trim().isEmpty()) { 251 setAnonymous(); 252 } else { 253 if (!newUserName.equals(userName)) { 254 setPartiallyIdentified(newUserName); 255 } 256 } 257 return; 258 259 case "osm-server.url": 260 String newUrl = null; 261 if (evt.getNewValue() instanceof StringSetting) { 262 newUrl = ((StringSetting) evt.getNewValue()).getValue(); 263 } 264 if (newUrl == null || newUrl.trim().isEmpty()) { 265 setAnonymous(); 266 } else if (isFullyIdentified()) { 267 setPartiallyIdentified(getUserName()); 268 } 269 break; 270 271 case "oauth.access-token.key": 272 accessTokenKeyChanged = true; 273 break; 274 275 case "oauth.access-token.secret": 276 accessTokenSecretChanged = true; 277 break; 278 } 279 280 if (accessTokenKeyChanged && accessTokenSecretChanged) { 281 accessTokenKeyChanged = false; 282 accessTokenSecretChanged = false; 283 if (OsmApi.isUsingOAuth()) { 284 try { 285 instance.initFromOAuth(Main.parent); 286 } catch (Exception e) { 287 Main.error(e); 288 } 289 } 290 } 291 } 292}