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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 6 weeks ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

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