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