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    public void visit(Visitor v) {
106        v.visit(this);
107    }
108
109    public int compareTo(Changeset other) {
110        return Integer.compare(getId(), other.getId());
111    }
112
113    public String getName() {
114        // no translation
115        return "changeset " + getId();
116    }
117
118    public String getDisplayName(NameFormatter formatter) {
119        return formatter.format(this);
120    }
121
122    public int getId() {
123        return id;
124    }
125
126    public void setId(int id) {
127        this.id = id;
128    }
129
130    public User getUser() {
131        return user;
132    }
133
134    public void setUser(User user) {
135        this.user = user;
136    }
137
138    public Date getCreatedAt() {
139        return createdAt;
140    }
141
142    public void setCreatedAt(Date createdAt) {
143        this.createdAt = createdAt;
144    }
145
146    public Date getClosedAt() {
147        return closedAt;
148    }
149
150    public void setClosedAt(Date closedAt) {
151        this.closedAt = closedAt;
152    }
153
154    public boolean isOpen() {
155        return open;
156    }
157
158    public void setOpen(boolean open) {
159        this.open = open;
160    }
161
162    public LatLon getMin() {
163        return min;
164    }
165
166    public void setMin(LatLon min) {
167        this.min = min;
168    }
169
170    public LatLon getMax() {
171        return max;
172    }
173
174    public Bounds getBounds() {
175        if (min != null && max != null)
176            return new Bounds(min, max);
177        return null;
178    }
179
180    public void setMax(LatLon max) {
181        this.max = max;
182    }
183
184    /**
185     * Replies the number of comments for this changeset.
186     * @return the number of comments for this changeset
187     * @since 7700
188     */
189    public int getCommentsCount() {
190        return commentsCount;
191    }
192
193    /**
194     * Sets the number of comments for this changeset.
195     * @param commentsCount the number of comments for this changeset
196     * @since 7700
197     */
198    public void setCommentsCount(int commentsCount) {
199        this.commentsCount = commentsCount;
200    }
201
202    @Override
203    public Map<String, String> getKeys() {
204        return tags;
205    }
206
207    @Override
208    public void setKeys(Map<String, String> keys) {
209        CheckParameterUtil.ensureParameterNotNull(keys, "keys");
210        keys.values().stream()
211                .filter(value -> value != null && value.length() > MAX_CHANGESET_TAG_LENGTH)
212                .findFirst()
213                .ifPresent(value -> {
214                throw new IllegalArgumentException("Changeset tag value is too long: "+value);
215        });
216        this.tags = keys;
217    }
218
219    public boolean isIncomplete() {
220        return incomplete;
221    }
222
223    public void setIncomplete(boolean incomplete) {
224        this.incomplete = incomplete;
225    }
226
227    @Override
228    public void put(String key, String value) {
229        CheckParameterUtil.ensureParameterNotNull(key, "key");
230        if (value != null && value.length() > MAX_CHANGESET_TAG_LENGTH) {
231            throw new IllegalArgumentException("Changeset tag value is too long: "+value);
232        }
233        this.tags.put(key, value);
234    }
235
236    @Override
237    public String get(String key) {
238        return this.tags.get(key);
239    }
240
241    @Override
242    public void remove(String key) {
243        this.tags.remove(key);
244    }
245
246    @Override
247    public void removeAll() {
248        this.tags.clear();
249    }
250
251    public boolean hasEqualSemanticAttributes(Changeset other) {
252        if (other == null)
253            return false;
254        if (closedAt == null) {
255            if (other.closedAt != null)
256                return false;
257        } else if (!closedAt.equals(other.closedAt))
258            return false;
259        if (createdAt == null) {
260            if (other.createdAt != null)
261                return false;
262        } else if (!createdAt.equals(other.createdAt))
263            return false;
264        if (id != other.id)
265            return false;
266        if (max == null) {
267            if (other.max != null)
268                return false;
269        } else if (!max.equals(other.max))
270            return false;
271        if (min == null) {
272            if (other.min != null)
273                return false;
274        } else if (!min.equals(other.min))
275            return false;
276        if (open != other.open)
277            return false;
278        if (tags == null) {
279            if (other.tags != null)
280                return false;
281        } else if (!tags.equals(other.tags))
282            return false;
283        if (user == null) {
284            if (other.user != null)
285                return false;
286        } else if (!user.equals(other.user))
287            return false;
288        if (commentsCount != other.commentsCount) {
289            return false;
290        }
291        return true;
292    }
293
294    @Override
295    public int hashCode() {
296        return Objects.hash(id);
297    }
298
299    @Override
300    public boolean equals(Object obj) {
301        if (this == obj) return true;
302        if (obj == null || getClass() != obj.getClass()) return false;
303        Changeset changeset = (Changeset) obj;
304        return id == changeset.id;
305    }
306
307    @Override
308    public boolean hasKeys() {
309        return !tags.keySet().isEmpty();
310    }
311
312    @Override
313    public Collection<String> keySet() {
314        return tags.keySet();
315    }
316
317    public boolean isNew() {
318        return id <= 0;
319    }
320
321    public void mergeFrom(Changeset other) {
322        if (other == null)
323            return;
324        if (id != other.id)
325            return;
326        this.user = other.user;
327        this.createdAt = other.createdAt;
328        this.closedAt = other.closedAt;
329        this.open = other.open;
330        this.min = other.min;
331        this.max = other.max;
332        this.commentsCount = other.commentsCount;
333        this.tags = new HashMap<>(other.tags);
334        this.incomplete = other.incomplete;
335        this.discussion = other.discussion != null ? new ArrayList<>(other.discussion) : null;
336
337        // FIXME: merging of content required?
338        this.content = other.content;
339    }
340
341    public boolean hasContent() {
342        return content != null;
343    }
344
345    public ChangesetDataSet getContent() {
346        return content;
347    }
348
349    public void setContent(ChangesetDataSet content) {
350        this.content = content;
351    }
352
353    /**
354     * Replies the list of comments in the changeset discussion, if any.
355     * @return the list of comments in the changeset discussion. May be empty but never null
356     * @since 7704
357     */
358    public synchronized List<ChangesetDiscussionComment> getDiscussion() {
359        if (discussion == null) {
360            return Collections.emptyList();
361        }
362        return new ArrayList<>(discussion);
363    }
364
365    /**
366     * Adds a comment to the changeset discussion.
367     * @param comment the comment to add. Ignored if null
368     * @since 7704
369     */
370    public synchronized void addDiscussionComment(ChangesetDiscussionComment comment) {
371        if (comment == null) {
372            return;
373        }
374        if (discussion == null) {
375            discussion = new ArrayList<>();
376        }
377        discussion.add(comment);
378    }
379}