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 > 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 <identifier>") 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}