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

Last change on this file since 7299 was 7299, checked in by Don-vip, 10 years ago

fix #10121 - Add a new look-and-feel preference to display ISO 8601 dates globally

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