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