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

Last change on this file since 10621 was 10615, checked in by Don-vip, 8 years ago

see #11390 - sonar - squid:S1604 - Java 8: Anonymous inner classes containing only one method should become lambdas

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