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}