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 number of changes for this changeset */ 046 private int changesCount; 047 /** the map of tags */ 048 private Map<String, String> tags; 049 /** indicates whether this changeset is incomplete. For an incomplete changeset we only know its id */ 050 private boolean incomplete; 051 /** the changeset content */ 052 private ChangesetDataSet content; 053 /** the changeset discussion */ 054 private List<ChangesetDiscussionComment> discussion; 055 056 /** 057 * Creates a new changeset with id 0. 058 */ 059 public Changeset() { 060 this(0); 061 } 062 063 /** 064 * Creates a changeset with id <code>id</code>. If id > 0, sets incomplete to true. 065 * 066 * @param id the id 067 */ 068 public Changeset(int id) { 069 this.id = id; 070 this.incomplete = id > 0; 071 this.tags = new HashMap<>(); 072 } 073 074 /** 075 * Creates a clone of <code>other</code> 076 * 077 * @param other the other changeset. If null, creates a new changeset with id 0. 078 */ 079 public Changeset(Changeset other) { 080 if (other == null) { 081 this.id = 0; 082 this.tags = new HashMap<>(); 083 } else if (other.isIncomplete()) { 084 setId(other.getId()); 085 this.incomplete = true; 086 this.tags = new HashMap<>(); 087 } else { 088 this.id = other.id; 089 mergeFrom(other); 090 this.incomplete = false; 091 } 092 } 093 094 /** 095 * Creates a changeset with the data obtained from the given preset, i.e., 096 * the {@link AbstractPrimitive#getChangesetId() changeset id}, {@link AbstractPrimitive#getUser() user}, and 097 * {@link AbstractPrimitive#getTimestamp() timestamp}. 098 * @param primitive the primitive to use 099 * @return the created changeset 100 */ 101 public static Changeset fromPrimitive(final OsmPrimitive primitive) { 102 final Changeset changeset = new Changeset(primitive.getChangesetId()); 103 changeset.setUser(primitive.getUser()); 104 changeset.setCreatedAt(primitive.getTimestamp()); // not accurate in all cases 105 return changeset; 106 } 107 108 /** 109 * Compares this changeset to another, based on their identifier. 110 * @param other other changeset 111 * @return the value {@code 0} if {@code getId() == other.getId()}; 112 * a value less than {@code 0} if {@code getId() < other.getId()}; and 113 * a value greater than {@code 0} if {@code getId() > other.getId()} 114 */ 115 @Override 116 public int compareTo(Changeset other) { 117 return Integer.compare(getId(), other.getId()); 118 } 119 120 /** 121 * Returns the changeset name. 122 * @return the changeset name (untranslated: "changeset <identifier>") 123 */ 124 public String getName() { 125 // no translation 126 return "changeset " + getId(); 127 } 128 129 /** 130 * Returns the changeset display name, as per given name formatter. 131 * @param formatter name formatter 132 * @return the changeset display name, as per given name formatter 133 */ 134 public String getDisplayName(NameFormatter formatter) { 135 return formatter.format(this); 136 } 137 138 /** 139 * Returns the changeset identifier. 140 * @return the changeset identifier 141 */ 142 public int getId() { 143 return id; 144 } 145 146 /** 147 * Sets the changeset identifier. 148 * @param id changeset identifier 149 */ 150 public void setId(int id) { 151 this.id = id; 152 } 153 154 /** 155 * Returns the changeset user. 156 * @return the changeset user 157 */ 158 public User getUser() { 159 return user; 160 } 161 162 /** 163 * Sets the changeset user. 164 * @param user changeset user 165 */ 166 public void setUser(User user) { 167 this.user = user; 168 } 169 170 /** 171 * Returns the changeset creation date. 172 * @return the changeset creation date 173 */ 174 public Date getCreatedAt() { 175 return DateUtils.cloneDate(createdAt); 176 } 177 178 /** 179 * Sets the changeset creation date. 180 * @param createdAt changeset creation date 181 */ 182 public void setCreatedAt(Date createdAt) { 183 this.createdAt = DateUtils.cloneDate(createdAt); 184 } 185 186 /** 187 * Returns the changeset closure date. 188 * @return the changeset closure date 189 */ 190 public Date getClosedAt() { 191 return DateUtils.cloneDate(closedAt); 192 } 193 194 /** 195 * Sets the changeset closure date. 196 * @param closedAt changeset closure date 197 */ 198 public void setClosedAt(Date closedAt) { 199 this.closedAt = DateUtils.cloneDate(closedAt); 200 } 201 202 /** 203 * Determines if this changeset is open. 204 * @return {@code true} if this changeset is open 205 */ 206 public boolean isOpen() { 207 return open; 208 } 209 210 /** 211 * Sets whether this changeset is open. 212 * @param open {@code true} if this changeset is open 213 */ 214 public void setOpen(boolean open) { 215 this.open = open; 216 } 217 218 /** 219 * Returns the min lat/lon of the changeset bounding box. 220 * @return the min lat/lon of the changeset bounding box 221 */ 222 public LatLon getMin() { 223 return min; 224 } 225 226 /** 227 * Sets the min lat/lon of the changeset bounding box. 228 * @param min min lat/lon of the changeset bounding box 229 */ 230 public void setMin(LatLon min) { 231 this.min = min; 232 } 233 234 /** 235 * Returns the max lat/lon of the changeset bounding box. 236 * @return the max lat/lon of the changeset bounding box 237 */ 238 public LatLon getMax() { 239 return max; 240 } 241 242 /** 243 * Sets the max lat/lon of the changeset bounding box. 244 * @param max min lat/lon of the changeset bounding box 245 */ 246 public void setMax(LatLon max) { 247 this.max = max; 248 } 249 250 /** 251 * Returns the changeset bounding box. 252 * @return the changeset bounding box 253 */ 254 public Bounds getBounds() { 255 if (min != null && max != null) 256 return new Bounds(min, max); 257 return null; 258 } 259 260 /** 261 * Replies this changeset comment. 262 * @return this changeset comment (empty string if missing) 263 * @since 12494 264 */ 265 public String getComment() { 266 return Optional.ofNullable(get("comment")).orElse(""); 267 } 268 269 /** 270 * Replies the number of comments for this changeset discussion. 271 * @return the number of comments for this changeset discussion 272 * @since 7700 273 */ 274 public int getCommentsCount() { 275 return commentsCount; 276 } 277 278 /** 279 * Sets the number of comments for this changeset discussion. 280 * @param commentsCount the number of comments for this changeset discussion 281 * @since 7700 282 */ 283 public void setCommentsCount(int commentsCount) { 284 this.commentsCount = commentsCount; 285 } 286 287 /** 288 * Replies the number of changes for this changeset. 289 * @return the number of changes for this changeset 290 * @since 14231 291 */ 292 public int getChangesCount() { 293 return changesCount; 294 } 295 296 /** 297 * Sets the number of changes for this changeset. 298 * @param changesCount the number of changes for this changeset 299 * @since 14231 300 */ 301 public void setChangesCount(int changesCount) { 302 this.changesCount = changesCount; 303 } 304 305 @Override 306 public Map<String, String> getKeys() { 307 return tags; 308 } 309 310 @Override 311 public void setKeys(Map<String, String> keys) { 312 CheckParameterUtil.ensureParameterNotNull(keys, "keys"); 313 keys.values().stream() 314 .filter(value -> value != null && value.length() > MAX_CHANGESET_TAG_LENGTH) 315 .findFirst() 316 .ifPresent(value -> { 317 throw new IllegalArgumentException("Changeset tag value is too long: "+value); 318 }); 319 this.tags = keys; 320 } 321 322 /** 323 * Determines if this changeset is incomplete. 324 * @return {@code true} if this changeset is incomplete 325 */ 326 public boolean isIncomplete() { 327 return incomplete; 328 } 329 330 /** 331 * Sets whether this changeset is incomplete 332 * @param incomplete {@code true} if this changeset is incomplete 333 */ 334 public void setIncomplete(boolean incomplete) { 335 this.incomplete = incomplete; 336 } 337 338 @Override 339 public void put(String key, String value) { 340 CheckParameterUtil.ensureParameterNotNull(key, "key"); 341 if (value != null && value.length() > MAX_CHANGESET_TAG_LENGTH) { 342 throw new IllegalArgumentException("Changeset tag value is too long: "+value); 343 } 344 this.tags.put(key, value); 345 } 346 347 @Override 348 public String get(String key) { 349 return this.tags.get(key); 350 } 351 352 @Override 353 public void remove(String key) { 354 this.tags.remove(key); 355 } 356 357 @Override 358 public void removeAll() { 359 this.tags.clear(); 360 } 361 362 /** 363 * Determines if this changeset has equals semantic attributes with another one. 364 * @param other other changeset 365 * @return {@code true} if this changeset has equals semantic attributes with other changeset 366 */ 367 public boolean hasEqualSemanticAttributes(Changeset other) { 368 return other != null 369 && id == other.id 370 && open == other.open 371 && commentsCount == other.commentsCount 372 && changesCount == other.changesCount 373 && Objects.equals(closedAt, other.closedAt) 374 && Objects.equals(createdAt, other.createdAt) 375 && Objects.equals(min, other.min) 376 && Objects.equals(max, other.max) 377 && Objects.equals(tags, other.tags) 378 && Objects.equals(user, other.user); 379 } 380 381 @Override 382 public int hashCode() { 383 return Objects.hash(id); 384 } 385 386 @Override 387 public boolean equals(Object obj) { 388 if (this == obj) return true; 389 if (obj == null || getClass() != obj.getClass()) return false; 390 Changeset changeset = (Changeset) obj; 391 return id == changeset.id; 392 } 393 394 @Override 395 public boolean hasKeys() { 396 return !tags.keySet().isEmpty(); 397 } 398 399 @Override 400 public Collection<String> keySet() { 401 return tags.keySet(); 402 } 403 404 @Override 405 public int getNumKeys() { 406 return tags.size(); 407 } 408 409 /** 410 * Determines if this changeset is new. 411 * @return {@code true} if this changeset is new ({@code id <= 0}) 412 */ 413 public boolean isNew() { 414 return id <= 0; 415 } 416 417 /** 418 * Merges changeset metadata from another changeset. 419 * @param other other changeset 420 */ 421 public void mergeFrom(Changeset other) { 422 if (other == null) 423 return; 424 if (id != other.id) 425 return; 426 this.user = other.user; 427 this.createdAt = DateUtils.cloneDate(other.createdAt); 428 this.closedAt = DateUtils.cloneDate(other.closedAt); 429 this.open = other.open; 430 this.min = other.min; 431 this.max = other.max; 432 this.commentsCount = other.commentsCount; 433 this.changesCount = other.changesCount; 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}