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