source: josm/trunk/src/org/openstreetmap/josm/io/ChangesetQuery.java@ 14630

Last change on this file since 14630 was 14095, checked in by Don-vip, 6 years ago

fix error_prone warnings

  • Property svn:eol-style set to native
File size: 24.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.text.DateFormat;
7import java.text.MessageFormat;
8import java.text.ParseException;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.Date;
13import java.util.HashMap;
14import java.util.Map;
15import java.util.Map.Entry;
16import java.util.stream.Collectors;
17import java.util.stream.Stream;
18
19import org.openstreetmap.josm.data.Bounds;
20import org.openstreetmap.josm.data.UserIdentityManager;
21import org.openstreetmap.josm.data.coor.LatLon;
22import org.openstreetmap.josm.tools.CheckParameterUtil;
23import org.openstreetmap.josm.tools.Logging;
24import org.openstreetmap.josm.tools.Utils;
25import org.openstreetmap.josm.tools.date.DateUtils;
26
27/**
28 * Data class to collect restrictions (parameters) for downloading changesets from the
29 * OSM API.
30 * <p>
31 * @see <a href="https://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_.2Fapi.2F0.6.2Fchangesets">OSM API 0.6 call "/changesets?"</a>
32 */
33public class ChangesetQuery {
34
35 /**
36 * Maximum number of changesets returned by the OSM API call "/changesets?"
37 */
38 public static final int MAX_CHANGESETS_NUMBER = 100;
39
40 /** the user id this query is restricted to. null, if no restriction to a user id applies */
41 private Integer uid;
42 /** the user name this query is restricted to. null, if no restriction to a user name applies */
43 private String userName;
44 /** the bounding box this query is restricted to. null, if no restriction to a bounding box applies */
45 private Bounds bounds;
46 /** the date after which changesets have been closed this query is restricted to. null, if no restriction to closure date applies */
47 private Date closedAfter;
48 /** the date before which changesets have been created this query is restricted to. null, if no restriction to creation date applies */
49 private Date createdBefore;
50 /** indicates whether only open changesets are queried. null, if no restrictions regarding open changesets apply */
51 private Boolean open;
52 /** indicates whether only closed changesets are queried. null, if no restrictions regarding closed changesets apply */
53 private Boolean closed;
54 /** a collection of changeset ids to query for */
55 private Collection<Long> changesetIds;
56
57 /**
58 * Replies a changeset query object from the query part of a OSM API URL for querying changesets.
59 *
60 * @param query the query part
61 * @return the query object
62 * @throws ChangesetQueryUrlException if query doesn't consist of valid query parameters
63 */
64 public static ChangesetQuery buildFromUrlQuery(String query) throws ChangesetQueryUrlException {
65 return new ChangesetQueryUrlParser().parse(query);
66 }
67
68 /**
69 * Replies a changeset query object restricted to the current user, if known.
70 * @return a changeset query object restricted to the current user, if known
71 * @throws IllegalStateException if current user is anonymous
72 * @since 12495
73 */
74 public static ChangesetQuery forCurrentUser() {
75 UserIdentityManager im = UserIdentityManager.getInstance();
76 if (im.isAnonymous()) {
77 throw new IllegalStateException("anonymous user");
78 }
79 ChangesetQuery query = new ChangesetQuery();
80 if (im.isFullyIdentified()) {
81 return query.forUser(im.getUserId());
82 } else {
83 return query.forUser(im.getUserName());
84 }
85 }
86
87 /**
88 * Restricts the query to changesets owned by the user with id <code>uid</code>.
89 *
90 * @param uid the uid of the user. &gt; 0 expected.
91 * @return the query object with the applied restriction
92 * @throws IllegalArgumentException if uid &lt;= 0
93 * @see #forUser(String)
94 */
95 public ChangesetQuery forUser(int uid) {
96 if (uid <= 0)
97 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected. Got ''{1}''.", "uid", uid));
98 this.uid = uid;
99 this.userName = null;
100 return this;
101 }
102
103 /**
104 * Restricts the query to changesets owned by the user with user name <code>username</code>.
105 *
106 * Caveat: for historical reasons the username might not be unique! It is recommended to use
107 * {@link #forUser(int)} to restrict the query to a specific user.
108 *
109 * @param userName the username. Must not be null.
110 * @return the query object with the applied restriction
111 * @throws IllegalArgumentException if username is null.
112 * @see #forUser(int)
113 */
114 public ChangesetQuery forUser(String userName) {
115 CheckParameterUtil.ensureParameterNotNull(userName, "userName");
116 this.userName = userName;
117 this.uid = null;
118 return this;
119 }
120
121 /**
122 * Replies true if this query is restricted to user whom we only know the user name for.
123 *
124 * @return true if this query is restricted to user whom we only know the user name for
125 */
126 public boolean isRestrictedToPartiallyIdentifiedUser() {
127 return userName != null;
128 }
129
130 /**
131 * Replies true/false if this query is restricted to changesets which are or aren't open.
132 *
133 * @return whether changesets should or should not be open, or {@code null} if there is no restriction
134 * @since 14039
135 */
136 public Boolean getRestrictionToOpen() {
137 return open;
138 }
139
140 /**
141 * Replies true/false if this query is restricted to changesets which are or aren't closed.
142 *
143 * @return whether changesets should or should not be closed, or {@code null} if there is no restriction
144 * @since 14039
145 */
146 public Boolean getRestrictionToClosed() {
147 return closed;
148 }
149
150 /**
151 * Replies the date after which changesets have been closed this query is restricted to.
152 *
153 * @return the date after which changesets have been closed this query is restricted to.
154 * {@code null}, if no restriction to closure date applies
155 * @since 14039
156 */
157 public Date getClosedAfter() {
158 return DateUtils.cloneDate(closedAfter);
159 }
160
161 /**
162 * Replies the date before which changesets have been created this query is restricted to.
163 *
164 * @return the date before which changesets have been created this query is restricted to.
165 * {@code null}, if no restriction to creation date applies
166 * @since 14039
167 */
168 public Date getCreatedBefore() {
169 return DateUtils.cloneDate(createdBefore);
170 }
171
172 /**
173 * Replies the list of additional changeset ids to query.
174 * @return the list of additional changeset ids to query (never null)
175 * @since 14039
176 */
177 public final Collection<Long> getAdditionalChangesetIds() {
178 return changesetIds != null ? new ArrayList<>(changesetIds) : Collections.emptyList();
179 }
180
181 /**
182 * Replies the bounding box this query is restricted to.
183 * @return the bounding box this query is restricted to. null, if no restriction to a bounding box applies
184 * @since 14039
185 */
186 public final Bounds getBounds() {
187 return bounds;
188 }
189
190 /**
191 * Replies the user name which this query is restricted to. null, if this query isn't
192 * restricted to a user name, i.e. if {@link #isRestrictedToPartiallyIdentifiedUser()} is false.
193 *
194 * @return the user name which this query is restricted to
195 */
196 public String getUserName() {
197 return userName;
198 }
199
200 /**
201 * Replies true if this query is restricted to user whom know the user id for.
202 *
203 * @return true if this query is restricted to user whom know the user id for
204 */
205 public boolean isRestrictedToFullyIdentifiedUser() {
206 return uid > 0;
207 }
208
209 /**
210 * Replies a query which is restricted to a bounding box.
211 *
212 * @param minLon min longitude of the bounding box. Valid longitude value expected.
213 * @param minLat min latitude of the bounding box. Valid latitude value expected.
214 * @param maxLon max longitude of the bounding box. Valid longitude value expected.
215 * @param maxLat max latitude of the bounding box. Valid latitude value expected.
216 *
217 * @return the restricted changeset query
218 * @throws IllegalArgumentException if either of the parameters isn't a valid longitude or
219 * latitude value
220 */
221 public ChangesetQuery inBbox(double minLon, double minLat, double maxLon, double maxLat) {
222 if (!LatLon.isValidLon(minLon))
223 throw new IllegalArgumentException(tr("Illegal longitude value for parameter ''{0}'', got {1}", "minLon", minLon));
224 if (!LatLon.isValidLon(maxLon))
225 throw new IllegalArgumentException(tr("Illegal longitude value for parameter ''{0}'', got {1}", "maxLon", maxLon));
226 if (!LatLon.isValidLat(minLat))
227 throw new IllegalArgumentException(tr("Illegal latitude value for parameter ''{0}'', got {1}", "minLat", minLat));
228 if (!LatLon.isValidLat(maxLat))
229 throw new IllegalArgumentException(tr("Illegal longitude value for parameter ''{0}'', got {1}", "maxLat", maxLat));
230
231 return inBbox(new LatLon(minLon, minLat), new LatLon(maxLon, maxLat));
232 }
233
234 /**
235 * Replies a query which is restricted to a bounding box.
236 *
237 * @param min the min lat/lon coordinates of the bounding box. Must not be null.
238 * @param max the max lat/lon coordiantes of the bounding box. Must not be null.
239 *
240 * @return the restricted changeset query
241 * @throws IllegalArgumentException if min is null
242 * @throws IllegalArgumentException if max is null
243 */
244 public ChangesetQuery inBbox(LatLon min, LatLon max) {
245 CheckParameterUtil.ensureParameterNotNull(min, "min");
246 CheckParameterUtil.ensureParameterNotNull(max, "max");
247 this.bounds = new Bounds(min, max);
248 return this;
249 }
250
251 /**
252 * Replies a query which is restricted to a bounding box given by <code>bbox</code>.
253 *
254 * @param bbox the bounding box. Must not be null.
255 * @return the changeset query
256 * @throws IllegalArgumentException if bbox is null.
257 */
258 public ChangesetQuery inBbox(Bounds bbox) {
259 CheckParameterUtil.ensureParameterNotNull(bbox, "bbox");
260 this.bounds = bbox;
261 return this;
262 }
263
264 /**
265 * Restricts the result to changesets which have been closed after the date given by <code>d</code>.
266 * <code>d</code> d is a date relative to the current time zone.
267 *
268 * @param d the date . Must not be null.
269 * @return the restricted changeset query
270 * @throws IllegalArgumentException if d is null
271 */
272 public ChangesetQuery closedAfter(Date d) {
273 CheckParameterUtil.ensureParameterNotNull(d, "d");
274 this.closedAfter = DateUtils.cloneDate(d);
275 return this;
276 }
277
278 /**
279 * Restricts the result to changesets which have been closed after <code>closedAfter</code> and which
280 * habe been created before <code>createdBefore</code>. Both dates are expressed relative to the current
281 * time zone.
282 *
283 * @param closedAfter only reply changesets closed after this date. Must not be null.
284 * @param createdBefore only reply changesets created before this date. Must not be null.
285 * @return the restricted changeset query
286 * @throws IllegalArgumentException if closedAfter is null
287 * @throws IllegalArgumentException if createdBefore is null
288 */
289 public ChangesetQuery closedAfterAndCreatedBefore(Date closedAfter, Date createdBefore) {
290 CheckParameterUtil.ensureParameterNotNull(closedAfter, "closedAfter");
291 CheckParameterUtil.ensureParameterNotNull(createdBefore, "createdBefore");
292 this.closedAfter = DateUtils.cloneDate(closedAfter);
293 this.createdBefore = DateUtils.cloneDate(createdBefore);
294 return this;
295 }
296
297 /**
298 * Restricts the result to changesets which are or aren't open, depending on the value of
299 * <code>isOpen</code>
300 *
301 * @param isOpen whether changesets should or should not be open
302 * @return the restricted changeset query
303 */
304 public ChangesetQuery beingOpen(boolean isOpen) {
305 this.open = isOpen;
306 return this;
307 }
308
309 /**
310 * Restricts the result to changesets which are or aren't closed, depending on the value of
311 * <code>isClosed</code>
312 *
313 * @param isClosed whether changesets should or should not be open
314 * @return the restricted changeset query
315 */
316 public ChangesetQuery beingClosed(boolean isClosed) {
317 this.closed = isClosed;
318 return this;
319 }
320
321 /**
322 * Restricts the query to the given changeset ids (which are added to previously added ones).
323 *
324 * @param changesetIds the changeset ids
325 * @return the query object with the applied restriction
326 * @throws IllegalArgumentException if changesetIds is null.
327 */
328 public ChangesetQuery forChangesetIds(Collection<Long> changesetIds) {
329 CheckParameterUtil.ensureParameterNotNull(changesetIds, "changesetIds");
330 if (changesetIds.size() > MAX_CHANGESETS_NUMBER) {
331 Logging.warn("Changeset query built with more than " + MAX_CHANGESETS_NUMBER + " changeset ids (" + changesetIds.size() + ')');
332 }
333 this.changesetIds = changesetIds;
334 return this;
335 }
336
337 /**
338 * Replies the query string to be used in a query URL for the OSM API.
339 *
340 * @return the query string
341 */
342 public String getQueryString() {
343 StringBuilder sb = new StringBuilder();
344 if (uid != null) {
345 sb.append("user=").append(uid);
346 } else if (userName != null) {
347 sb.append("display_name=").append(Utils.encodeUrl(userName));
348 }
349 if (bounds != null) {
350 if (sb.length() > 0) {
351 sb.append('&');
352 }
353 sb.append("bbox=").append(bounds.encodeAsString(","));
354 }
355 if (closedAfter != null && createdBefore != null) {
356 if (sb.length() > 0) {
357 sb.append('&');
358 }
359 DateFormat df = DateUtils.newIsoDateTimeFormat();
360 sb.append("time=").append(df.format(closedAfter));
361 sb.append(',').append(df.format(createdBefore));
362 } else if (closedAfter != null) {
363 if (sb.length() > 0) {
364 sb.append('&');
365 }
366 DateFormat df = DateUtils.newIsoDateTimeFormat();
367 sb.append("time=").append(df.format(closedAfter));
368 }
369
370 if (open != null) {
371 if (sb.length() > 0) {
372 sb.append('&');
373 }
374 sb.append("open=").append(Boolean.toString(open));
375 } else if (closed != null) {
376 if (sb.length() > 0) {
377 sb.append('&');
378 }
379 sb.append("closed=").append(Boolean.toString(closed));
380 } else if (changesetIds != null) {
381 // since 2013-12-05, see https://github.com/openstreetmap/openstreetmap-website/commit/1d1f194d598e54a5d6fb4f38fb569d4138af0dc8
382 if (sb.length() > 0) {
383 sb.append('&');
384 }
385 sb.append("changesets=").append(Utils.join(",", changesetIds));
386 }
387 return sb.toString();
388 }
389
390 @Override
391 public String toString() {
392 return getQueryString();
393 }
394
395 /**
396 * Exception thrown for invalid changeset queries.
397 */
398 public static class ChangesetQueryUrlException extends Exception {
399
400 /**
401 * Constructs a new {@code ChangesetQueryUrlException} with the specified detail message.
402 *
403 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
404 */
405 public ChangesetQueryUrlException(String message) {
406 super(message);
407 }
408
409 /**
410 * Constructs a new {@code ChangesetQueryUrlException} with the specified cause and detail message.
411 *
412 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
413 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
414 * (A <code>null</code> value is permitted, and indicates that the cause is nonexistent or unknown.)
415 */
416 public ChangesetQueryUrlException(String message, Throwable cause) {
417 super(message, cause);
418 }
419
420 /**
421 * Constructs a new {@code ChangesetQueryUrlException} with the specified cause and a detail message of
422 * <code>(cause==null ? null : cause.toString())</code> (which typically contains the class and detail message of <code>cause</code>).
423 *
424 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
425 * (A <code>null</code> value is permitted, and indicates that the cause is nonexistent or unknown.)
426 */
427 public ChangesetQueryUrlException(Throwable cause) {
428 super(cause);
429 }
430 }
431
432 /**
433 * Changeset query URL parser.
434 */
435 public static class ChangesetQueryUrlParser {
436 protected int parseUid(String value) throws ChangesetQueryUrlException {
437 if (value == null || value.trim().isEmpty())
438 throw new ChangesetQueryUrlException(
439 tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "uid", value));
440 int id;
441 try {
442 id = Integer.parseInt(value);
443 if (id <= 0)
444 throw new ChangesetQueryUrlException(
445 tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "uid", value));
446 } catch (NumberFormatException e) {
447 throw new ChangesetQueryUrlException(
448 tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "uid", value), e);
449 }
450 return id;
451 }
452
453 protected boolean parseBoolean(String value, String parameter) throws ChangesetQueryUrlException {
454 if (value == null || value.trim().isEmpty())
455 throw new ChangesetQueryUrlException(
456 tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter, value));
457 switch (value) {
458 case "true":
459 return true;
460 case "false":
461 return false;
462 default:
463 throw new ChangesetQueryUrlException(
464 tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter, value));
465 }
466 }
467
468 protected Date parseDate(String value, String parameter) throws ChangesetQueryUrlException {
469 if (value == null || value.trim().isEmpty())
470 throw new ChangesetQueryUrlException(
471 tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter, value));
472 DateFormat formatter = DateUtils.newIsoDateTimeFormat();
473 try {
474 return formatter.parse(value);
475 } catch (ParseException e) {
476 throw new ChangesetQueryUrlException(
477 tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter, value), e);
478 }
479 }
480
481 protected Date[] parseTime(String value) throws ChangesetQueryUrlException {
482 String[] dates = value.split(",");
483 if (dates.length == 0 || dates.length > 2)
484 throw new ChangesetQueryUrlException(
485 tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "time", value));
486 if (dates.length == 1)
487 return new Date[]{parseDate(dates[0], "time")};
488 else if (dates.length == 2)
489 return new Date[]{parseDate(dates[0], "time"), parseDate(dates[1], "time")};
490 return new Date[]{};
491 }
492
493 protected Collection<Long> parseLongs(String value) {
494 if (value == null || value.isEmpty()) {
495 return Collections.<Long>emptySet();
496 } else {
497 return Stream.of(value.split(",")).map(Long::valueOf).collect(Collectors.toSet());
498 }
499 }
500
501 protected ChangesetQuery createFromMap(Map<String, String> queryParams) throws ChangesetQueryUrlException {
502 ChangesetQuery csQuery = new ChangesetQuery();
503
504 for (Entry<String, String> entry: queryParams.entrySet()) {
505 String k = entry.getKey();
506 switch(k) {
507 case "uid":
508 if (queryParams.containsKey("display_name"))
509 throw new ChangesetQueryUrlException(
510 tr("Cannot create a changeset query including both the query parameters ''uid'' and ''display_name''"));
511 csQuery.forUser(parseUid(queryParams.get("uid")));
512 break;
513 case "display_name":
514 if (queryParams.containsKey("uid"))
515 throw new ChangesetQueryUrlException(
516 tr("Cannot create a changeset query including both the query parameters ''uid'' and ''display_name''"));
517 csQuery.forUser(queryParams.get("display_name"));
518 break;
519 case "open":
520 csQuery.beingOpen(parseBoolean(entry.getValue(), "open"));
521 break;
522 case "closed":
523 csQuery.beingClosed(parseBoolean(entry.getValue(), "closed"));
524 break;
525 case "time":
526 Date[] dates = parseTime(entry.getValue());
527 switch(dates.length) {
528 case 1:
529 csQuery.closedAfter(dates[0]);
530 break;
531 case 2:
532 csQuery.closedAfterAndCreatedBefore(dates[0], dates[1]);
533 break;
534 default:
535 Logging.warn("Unable to parse time: " + entry.getValue());
536 }
537 break;
538 case "bbox":
539 try {
540 csQuery.inBbox(new Bounds(entry.getValue(), ","));
541 } catch (IllegalArgumentException e) {
542 throw new ChangesetQueryUrlException(e);
543 }
544 break;
545 case "changesets":
546 try {
547 csQuery.forChangesetIds(parseLongs(entry.getValue()));
548 } catch (NumberFormatException e) {
549 throw new ChangesetQueryUrlException(e);
550 }
551 break;
552 default:
553 throw new ChangesetQueryUrlException(
554 tr("Unsupported parameter ''{0}'' in changeset query string", k));
555 }
556 }
557 return csQuery;
558 }
559
560 protected Map<String, String> createMapFromQueryString(String query) {
561 Map<String, String> queryParams = new HashMap<>();
562 String[] keyValuePairs = query.split("&");
563 for (String keyValuePair: keyValuePairs) {
564 String[] kv = keyValuePair.split("=");
565 queryParams.put(kv[0], kv.length > 1 ? kv[1] : "");
566 }
567 return queryParams;
568 }
569
570 /**
571 * Parses the changeset query given as URL query parameters and replies a {@link ChangesetQuery}.
572 *
573 * <code>query</code> is the query part of a API url for querying changesets,
574 * see <a href="http://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_.2Fapi.2F0.6.2Fchangesets">OSM API</a>.
575 *
576 * Example for an query string:<br>
577 * <pre>
578 * uid=1234&amp;open=true
579 * </pre>
580 *
581 * @param query the query string. If null, an empty query (identical to a query for all changesets) is assumed
582 * @return the changeset query
583 * @throws ChangesetQueryUrlException if the query string doesn't represent a legal query for changesets
584 */
585 public ChangesetQuery parse(String query) throws ChangesetQueryUrlException {
586 if (query == null)
587 return new ChangesetQuery();
588 String apiQuery = query.trim();
589 if (apiQuery.isEmpty())
590 return new ChangesetQuery();
591 return createFromMap(createMapFromQueryString(apiQuery));
592 }
593 }
594}
Note: See TracBrowser for help on using the repository browser.