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