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 > 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}