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, 15 months 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.