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