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.time.Instant;
007import java.util.ArrayList;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.HashMap;
011import java.util.List;
012import java.util.Map;
013import java.util.Objects;
014import java.util.Optional;
015
016import org.openstreetmap.josm.data.Bounds;
017import org.openstreetmap.josm.data.coor.LatLon;
018import org.openstreetmap.josm.tools.CheckParameterUtil;
019
020/**
021 * Represents a single changeset in JOSM. For now its only used during
022 * upload but in the future we may do more.
023 * @since 625
024 */
025public final class Changeset implements Tagged, Comparable<Changeset> {
026
027    /** The maximum changeset tag length allowed by API 0.6 **/
028    public static final int MAX_CHANGESET_TAG_LENGTH = MAX_TAG_LENGTH;
029
030    /** the changeset id */
031    private int id;
032    /** the user who owns the changeset */
033    private User user;
034    /** date this changeset was created at */
035    private Instant createdAt;
036    /** the date this changeset was closed at*/
037    private Instant closedAt;
038    /** indicates whether this changeset is still open or not */
039    private boolean open;
040    /** the min. coordinates of the bounding box of this changeset */
041    private LatLon min;
042    /** the max. coordinates of the bounding box of this changeset */
043    private LatLon max;
044    /** the number of comments for this changeset */
045    private int commentsCount;
046    /** the number of changes for this changeset */
047    private int changesCount;
048    /** the map of tags */
049    private Map<String, String> tags;
050    /** indicates whether this changeset is incomplete. For an incomplete changeset we only know its id */
051    private boolean incomplete;
052    /** the changeset content */
053    private ChangesetDataSet content;
054    /** the changeset discussion */
055    private List<ChangesetDiscussionComment> discussion;
056
057    /**
058     * Creates a new changeset with id 0.
059     */
060    public Changeset() {
061        this(0);
062    }
063
064    /**
065     * Creates a changeset with id <code>id</code>. If id &gt; 0, sets incomplete to true.
066     *
067     * @param id the id
068     */
069    public Changeset(int id) {
070        this.id = id;
071        this.incomplete = id > 0;
072        this.tags = new HashMap<>();
073    }
074
075    /**
076     * Creates a clone of <code>other</code>
077     *
078     * @param other the other changeset. If null, creates a new changeset with id 0.
079     */
080    public Changeset(Changeset other) {
081        if (other == null) {
082            this.id = 0;
083            this.tags = new HashMap<>();
084        } else if (other.isIncomplete()) {
085            setId(other.getId());
086            this.incomplete = true;
087            this.tags = new HashMap<>();
088        } else {
089            this.id = other.id;
090            mergeFrom(other);
091            this.incomplete = false;
092        }
093    }
094
095    /**
096     * Creates a changeset with the data obtained from the given preset, i.e.,
097     * the {@link AbstractPrimitive#getChangesetId() changeset id}, {@link AbstractPrimitive#getUser() user}, and
098     * {@link AbstractPrimitive#getTimestamp() timestamp}.
099     * @param primitive the primitive to use
100     * @return the created changeset
101     */
102    public static Changeset fromPrimitive(final OsmPrimitive primitive) {
103        final Changeset changeset = new Changeset(primitive.getChangesetId());
104        changeset.setUser(primitive.getUser());
105        changeset.setCreatedAt(primitive.getInstant()); // not accurate in all cases
106        return changeset;
107    }
108
109    /**
110     * Compares this changeset to another, based on their identifier.
111     * @param other other changeset
112     * @return the value {@code 0} if {@code getId() == other.getId()};
113     *         a value less than {@code 0} if {@code getId() < other.getId()}; and
114     *         a value greater than {@code 0} if {@code getId() > other.getId()}
115     */
116    @Override
117    public int compareTo(Changeset other) {
118        return Integer.compare(getId(), other.getId());
119    }
120
121    /**
122     * Returns the changeset name.
123     * @return the changeset name (untranslated: "changeset &lt;identifier&gt;")
124     */
125    public String getName() {
126        // no translation
127        return "changeset " + getId();
128    }
129
130    /**
131     * Returns the changeset display name, as per given name formatter.
132     * @param formatter name formatter
133     * @return the changeset display name, as per given name formatter
134     */
135    public String getDisplayName(NameFormatter formatter) {
136        return formatter.format(this);
137    }
138
139    /**
140     * Returns the changeset identifier.
141     * @return the changeset identifier
142     */
143    public int getId() {
144        return id;
145    }
146
147    /**
148     * Sets the changeset identifier.
149     * @param id changeset identifier
150     */
151    public void setId(int id) {
152        this.id = id;
153    }
154
155    /**
156     * Returns the changeset user.
157     * @return the changeset user
158     */
159    public User getUser() {
160        return user;
161    }
162
163    /**
164     * Sets the changeset user.
165     * @param user changeset user
166     */
167    public void setUser(User user) {
168        this.user = user;
169    }
170
171    /**
172     * Returns the changeset creation date.
173     * @return the changeset creation date
174     */
175    public Instant getCreatedAt() {
176        return createdAt;
177    }
178
179    /**
180     * Sets the changeset creation date.
181     * @param createdAt changeset creation date
182     */
183    public void setCreatedAt(Instant createdAt) {
184        this.createdAt = createdAt;
185    }
186
187    /**
188     * Returns the changeset closure date.
189     * @return the changeset closure date
190     */
191    public Instant getClosedAt() {
192        return closedAt;
193    }
194
195    /**
196     * Sets the changeset closure date.
197     * @param closedAt changeset closure date
198     */
199    public void setClosedAt(Instant closedAt) {
200        this.closedAt = closedAt;
201    }
202
203    /**
204     * Determines if this changeset is open.
205     * @return {@code true} if this changeset is open
206     */
207    public boolean isOpen() {
208        return open;
209    }
210
211    /**
212     * Sets whether this changeset is open.
213     * @param open {@code true} if this changeset is open
214     */
215    public void setOpen(boolean open) {
216        this.open = open;
217    }
218
219    /**
220     * Returns the min lat/lon of the changeset bounding box.
221     * @return the min lat/lon of the changeset bounding box
222     */
223    public LatLon getMin() {
224        return min;
225    }
226
227    /**
228     * Sets the min lat/lon of the changeset bounding box.
229     * @param min min lat/lon of the changeset bounding box
230     */
231    public void setMin(LatLon min) {
232        this.min = min;
233    }
234
235    /**
236     * Returns the max lat/lon of the changeset bounding box.
237     * @return the max lat/lon of the changeset bounding box
238     */
239    public LatLon getMax() {
240        return max;
241    }
242
243    /**
244     * Sets the max lat/lon of the changeset bounding box.
245     * @param max min lat/lon of the changeset bounding box
246     */
247    public void setMax(LatLon max) {
248        this.max = max;
249    }
250
251    /**
252     * Returns the changeset bounding box.
253     * @return the changeset bounding box
254     */
255    public Bounds getBounds() {
256        if (min != null && max != null)
257            return new Bounds(min, max);
258        return null;
259    }
260
261    /**
262     * Replies this changeset comment.
263     * @return this changeset comment (empty string if missing)
264     * @since 12494
265     */
266    public String getComment() {
267        return Optional.ofNullable(get("comment")).orElse("");
268    }
269
270    /**
271     * Replies the number of comments for this changeset discussion.
272     * @return the number of comments for this changeset discussion
273     * @since 7700
274     */
275    public int getCommentsCount() {
276        return commentsCount;
277    }
278
279    /**
280     * Sets the number of comments for this changeset discussion.
281     * @param commentsCount the number of comments for this changeset discussion
282     * @since 7700
283     */
284    public void setCommentsCount(int commentsCount) {
285        this.commentsCount = commentsCount;
286    }
287
288    /**
289     * Replies the number of changes for this changeset.
290     * @return the number of changes for this changeset
291     * @since 14231
292     */
293    public int getChangesCount() {
294        return changesCount;
295    }
296
297    /**
298     * Sets the number of changes for this changeset.
299     * @param changesCount the number of changes for this changeset
300     * @since 14231
301     */
302    public void setChangesCount(int changesCount) {
303        this.changesCount = changesCount;
304    }
305
306    @Override
307    public Map<String, String> getKeys() {
308        return tags;
309    }
310
311    @Override
312    public void setKeys(Map<String, String> keys) {
313        CheckParameterUtil.ensureParameterNotNull(keys, "keys");
314        keys.values().stream()
315                .filter(value -> value != null && value.length() > MAX_CHANGESET_TAG_LENGTH)
316                .findFirst()
317                .ifPresent(value -> {
318                throw new IllegalArgumentException("Changeset tag value is too long: "+value);
319        });
320        this.tags = new HashMap<>(keys);
321    }
322
323    /**
324     * Determines if this changeset is incomplete.
325     * @return {@code true} if this changeset is incomplete
326     */
327    public boolean isIncomplete() {
328        return incomplete;
329    }
330
331    /**
332     * Sets whether this changeset is incomplete
333     * @param incomplete {@code true} if this changeset is incomplete
334     */
335    public void setIncomplete(boolean incomplete) {
336        this.incomplete = incomplete;
337    }
338
339    @Override
340    public void put(String key, String value) {
341        CheckParameterUtil.ensureParameterNotNull(key, "key");
342        if (value != null && value.length() > MAX_CHANGESET_TAG_LENGTH) {
343            throw new IllegalArgumentException("Changeset tag value is too long: "+value);
344        }
345        this.tags.put(key, value);
346    }
347
348    @Override
349    public String get(String key) {
350        return this.tags.get(key);
351    }
352
353    @Override
354    public void remove(String key) {
355        this.tags.remove(key);
356    }
357
358    @Override
359    public void removeAll() {
360        this.tags.clear();
361    }
362
363    /**
364     * Determines if this changeset has equals semantic attributes with another one.
365     * @param other other changeset
366     * @return {@code true} if this changeset has equals semantic attributes with other changeset
367     */
368    public boolean hasEqualSemanticAttributes(Changeset other) {
369        return other != null
370            && id == other.id
371            && open == other.open
372            && commentsCount == other.commentsCount
373            && changesCount == other.changesCount
374            && Objects.equals(closedAt, other.closedAt)
375            && Objects.equals(createdAt, other.createdAt)
376            && Objects.equals(min, other.min)
377            && Objects.equals(max, other.max)
378            && Objects.equals(tags, other.tags)
379            && Objects.equals(user, other.user);
380    }
381
382    @Override
383    public int hashCode() {
384        return Objects.hash(id);
385    }
386
387    @Override
388    public boolean equals(Object obj) {
389        if (this == obj) return true;
390        if (obj == null || getClass() != obj.getClass()) return false;
391        Changeset changeset = (Changeset) obj;
392        return id == changeset.id;
393    }
394
395    @Override
396    public boolean hasKeys() {
397        return !tags.keySet().isEmpty();
398    }
399
400    @Override
401    public Collection<String> keySet() {
402        return tags.keySet();
403    }
404
405    @Override
406    public int getNumKeys() {
407        return tags.size();
408    }
409
410    /**
411     * Determines if this changeset is new.
412     * @return {@code true} if this changeset is new ({@code id <= 0})
413     */
414    public boolean isNew() {
415        return id <= 0;
416    }
417
418    /**
419     * Merges changeset metadata from another changeset.
420     * @param other other changeset
421     */
422    public void mergeFrom(Changeset other) {
423        if (other == null)
424            return;
425        if (id != other.id)
426            return;
427        this.user = other.user;
428        this.createdAt = other.createdAt;
429        this.closedAt = other.closedAt;
430        this.open = other.open;
431        this.min = other.min;
432        this.max = other.max;
433        this.commentsCount = other.commentsCount;
434        this.changesCount = other.changesCount;
435        this.tags = new HashMap<>(other.tags);
436        this.incomplete = other.incomplete;
437        this.discussion = other.discussion != null ? new ArrayList<>(other.discussion) : null;
438
439        // FIXME: merging of content required?
440        this.content = other.content;
441    }
442
443    /**
444     * Determines if this changeset has contents.
445     * @return {@code true} if this changeset has contents
446     */
447    public boolean hasContent() {
448        return content != null;
449    }
450
451    /**
452     * Returns the changeset contents.
453     * @return the changeset contents, can be null
454     */
455    public ChangesetDataSet getContent() {
456        return content;
457    }
458
459    /**
460     * Sets the changeset contents.
461     * @param content changeset contents, can be null
462     */
463    public void setContent(ChangesetDataSet content) {
464        this.content = content;
465    }
466
467    /**
468     * Replies the list of comments in the changeset discussion, if any.
469     * @return the list of comments in the changeset discussion. May be empty but never null
470     * @since 7704
471     */
472    public synchronized List<ChangesetDiscussionComment> getDiscussion() {
473        if (discussion == null) {
474            return Collections.emptyList();
475        }
476        return Collections.unmodifiableList(discussion);
477    }
478
479    /**
480     * Adds a comment to the changeset discussion.
481     * @param comment the comment to add. Ignored if null
482     * @since 7704
483     */
484    public synchronized void addDiscussionComment(ChangesetDiscussionComment comment) {
485        if (comment == null) {
486            return;
487        }
488        if (discussion == null) {
489            discussion = new ArrayList<>();
490        }
491        discussion.add(comment);
492    }
493
494    @Override
495    public String toString() {
496        return tr("Changeset") + " " + id + ": " + getComment();
497    }
498}