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

Last change on this file since 12470 was 12470, checked in by bastiK, 7 years ago

see #14794 - javadoc

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