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            return lastUser;
073        }
074
075        Long ouid = uid;
076        User user = userMap.get(ouid);
077        if (user == null) {
078            user = new User(uid, name);
079            userMap.put(ouid, user);
080        }
081        if (name != null) user.addName(name);
082
083        lastUser = user;
084
085        return user;
086    }
087
088    /**
089     * clears the static map of user ids to user objects
090     */
091    public static synchronized void clearUserMap() {
092        userMap.clear();
093    }
094
095    /**
096     * Returns the user with user id <code>uid</code> or null if this user doesn't exist
097     *
098     * @param uid the user id
099     * @return the user; null, if there is no user with  this id
100     */
101    public static synchronized User getById(long uid) {
102        return userMap.get(uid);
103    }
104
105    /**
106     * Returns the list of users with name <code>name</code> or the empty list if
107     * no such users exist
108     *
109     * @param name the user name
110     * @return the list of users with name <code>name</code> or the empty list if
111     * no such users exist
112     */
113    public static synchronized List<User> getByName(String name) {
114        if (name == null) {
115            name = "";
116        }
117        List<User> ret = new ArrayList<>();
118        for (User user: userMap.values()) {
119            if (user.hasName(name)) {
120                ret.add(user);
121            }
122        }
123        return ret;
124    }
125
126    /**
127     * Replies the anonymous user
128     * @return The anonymous user
129     */
130    public static User getAnonymous() {
131        return anonymous;
132    }
133
134    /** the user name */
135    private final LinkedHashSet<String> names = new LinkedHashSet<>();
136    /** the user id */
137    private final long uid;
138
139    /**
140     * Replies the user name
141     *
142     * @return the user name. Never <code>null</code>, but may be the empty string
143     * @see #getByName(String)
144     * @see #createOsmUser(long, String)
145     * @see #createLocalUser(String)
146     */
147    public String getName() {
148        return names.isEmpty() ? "" : names.iterator().next();
149    }
150
151    /**
152     * Returns the list of user names
153     *
154     * @return list of names
155     */
156    public List<String> getNames() {
157        return new ArrayList<>(names);
158    }
159
160    /**
161     * Adds a user name to the list if it is not there, yet.
162     *
163     * @param name User name
164     */
165    public void addName(String name) {
166        names.add(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     */
176    public void setPreferredName(String name) {
177        if (names.size() == 1 && names.contains(name)) {
178            return;
179        }
180        final Collection<String> allNames = new LinkedHashSet<>(names);
181        names.clear();
182        names.add(name);
183        names.addAll(allNames);
184    }
185
186    /**
187     * Returns true if the name is in the names list
188     *
189     * @param name User name
190     * @return <code>true</code> if the name is in the names list
191     */
192    public boolean hasName(String name) {
193        return names.contains(name);
194    }
195
196    /**
197     * Replies the user id. If this user is known to the OSM server the positive user id
198     * from the server is replied. Otherwise, a negative local value is replied.
199     *
200     * A negative local is only unique during an editing session. It is lost when the
201     * application is closed and there is no guarantee that a negative local user id is
202     * always bound to a user with the same name.
203     *
204     * @return the user id
205     */
206    public long getId() {
207        return uid;
208    }
209
210    /**
211     * Private constructor, only called from get method.
212     * @param uid user id
213     * @param name user name
214     */
215    private User(long uid, String name) {
216        this.uid = uid;
217        if (name != null) {
218            addName(name);
219        }
220    }
221
222    /**
223     * Determines if this user is known to OSM
224     * @return {@code true} if this user is known to OSM, {@code false} otherwise
225     */
226    public boolean isOsmUser() {
227        return uid > 0;
228    }
229
230    /**
231     * Determines if this user is local
232     * @return {@code true} if this user is local, {@code false} otherwise
233     */
234    public boolean isLocalUser() {
235        return uid < 0;
236    }
237
238    @Override
239    public int hashCode() {
240        return Objects.hash(uid);
241    }
242
243    @Override
244    public boolean equals(Object obj) {
245        if (this == obj) return true;
246        if (obj == null || getClass() != obj.getClass()) return false;
247        User user = (User) obj;
248        return uid == user.uid;
249    }
250
251    @Override
252    public String toString() {
253        StringBuilder s = new StringBuilder();
254        s.append("id:").append(uid);
255        if (names.size() == 1) {
256            s.append(" name:").append(getName());
257        } else if (names.size() > 1) {
258            s.append(String.format(" %d names:%s", names.size(), getName()));
259        }
260        return s.toString();
261    }
262}