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 * JosmUserStateManager 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 JosmUserStateManager 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 JosmUserStateManager know in case they fully identify the current user, see
042 * {@link #setFullyIdentified}.
043 *
044 * The information kept by the JosmUserStateManager 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    static private 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    static public 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 (Throwable t) {
067                    t.printStackTrace();
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 e) {
220            e.printStackTrace();
221        } catch (OsmTransferException e) {
222            e.printStackTrace();
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        if (evt.getKey().equals("osm-server.username")) {
245            if (!(evt.getNewValue() instanceof StringSetting)) return;
246            String newValue = ((StringSetting) evt.getNewValue()).getValue();
247            if (newValue == null || newValue.trim().length() == 0) {
248                setAnonymous();
249            } else {
250                if (! newValue.equals(userName)) {
251                    setPartiallyIdentified(newValue);
252                }
253            }
254            return;
255
256        } else if (evt.getKey().equals("osm-server.url")) {
257            if (!(evt.getNewValue() instanceof StringSetting)) return;
258            String newValue = ((StringSetting) evt.getNewValue()).getValue();
259            if (newValue == null || newValue.trim().isEmpty()) {
260                setAnonymous();
261            } else if (isFullyIdentified()) {
262                setPartiallyIdentified(getUserName());
263            }
264
265        } else if (evt.getKey().equals("oauth.access-token.key")) {
266            accessTokenKeyChanged = true;
267
268        } else if (evt.getKey().equals("oauth.access-token.secret")) {
269            accessTokenSecretChanged = true;
270        }
271
272        if (accessTokenKeyChanged && accessTokenSecretChanged) {
273            accessTokenKeyChanged = false;
274            accessTokenSecretChanged = false;
275            if (OsmApi.isUsingOAuth()) {
276                try {
277                    instance.initFromOAuth(Main.parent);
278                } catch (Throwable t) {
279                    t.printStackTrace();
280                }
281            }
282        }
283    }
284}