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

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

see #11924, see #15560, see #16048 - tt HTML tag is deprecated in HTML5: use code instead

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