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

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

see #10121 - fix handling of time, causing an error in unit tests

  • Property svn:eol-style set to native
File size: 20.2 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 DateFormat formatter = DateUtils.newIsoDateTimeFormat();
368 try {
369 return formatter.parse(value);
370 } catch(ParseException e) {
371 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter, value));
372 }
373 }
374
375 protected Date[] parseTime(String value) throws ChangesetQueryUrlException {
376 String[] dates = value.split(",");
377 if (dates == null || dates.length == 0 || dates.length > 2)
378 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "time", value));
379 if (dates.length == 1)
380 return new Date[]{parseDate(dates[0], "time")};
381 else if (dates.length == 2)
382 return new Date[]{parseDate(dates[0], "time"), parseDate(dates[1], "time")};
383 return null;
384 }
385
386 protected Collection<Long> parseLongs(String value) {
387 return value == null || value.isEmpty()
388 ? Collections.<Long>emptySet() :
389 new HashSet<>(Utils.transform(Arrays.asList(value.split(",")), new Utils.Function<String, Long>() {
390 @Override
391 public Long apply(String x) {
392 return Long.valueOf(x);
393 }
394 }));
395 }
396
397 protected ChangesetQuery createFromMap(Map<String, String> queryParams) throws ChangesetQueryUrlException {
398 ChangesetQuery csQuery = new ChangesetQuery();
399
400 for (Entry<String, String> entry: queryParams.entrySet()) {
401 String k = entry.getKey();
402 switch(k) {
403 case "uid":
404 if (queryParams.containsKey("display_name"))
405 throw new ChangesetQueryUrlException(tr("Cannot create a changeset query including both the query parameters ''uid'' and ''display_name''"));
406 csQuery.forUser(parseUid(queryParams.get("uid")));
407 break;
408 case "display_name":
409 if (queryParams.containsKey("uid"))
410 throw new ChangesetQueryUrlException(tr("Cannot create a changeset query including both the query parameters ''uid'' and ''display_name''"));
411 csQuery.forUser(queryParams.get("display_name"));
412 break;
413 case "open":
414 csQuery.beingOpen(parseBoolean(entry.getValue(), "open"));
415 break;
416 case "closed":
417 csQuery.beingClosed(parseBoolean(entry.getValue(), "closed"));
418 break;
419 case "time":
420 Date[] dates = parseTime(entry.getValue());
421 switch(dates.length) {
422 case 1:
423 csQuery.closedAfter(dates[0]);
424 break;
425 case 2:
426 csQuery.closedAfterAndCreatedBefore(dates[0], dates[1]);
427 break;
428 }
429 break;
430 case "bbox":
431 try {
432 csQuery.inBbox(new Bounds(entry.getValue(), ","));
433 } catch (IllegalArgumentException e) {
434 throw new ChangesetQueryUrlException(e);
435 }
436 break;
437 case "changesets":
438 try {
439 csQuery.forChangesetIds(parseLongs(entry.getValue()));
440 } catch (NumberFormatException e) {
441 throw new ChangesetQueryUrlException(e);
442 }
443 break;
444 default:
445 throw new ChangesetQueryUrlException(tr("Unsupported parameter ''{0}'' in changeset query string", k));
446 }
447 }
448 return csQuery;
449 }
450
451 protected Map<String,String> createMapFromQueryString(String query) {
452 Map<String,String> queryParams = new HashMap<>();
453 String[] keyValuePairs = query.split("&");
454 for (String keyValuePair: keyValuePairs) {
455 String[] kv = keyValuePair.split("=");
456 queryParams.put(kv[0], kv.length > 1 ? kv[1] : "");
457 }
458 return queryParams;
459 }
460
461 /**
462 * Parses the changeset query given as URL query parameters and replies a {@link ChangesetQuery}.
463 *
464 * <code>query</code> is the query part of a API url for querying changesets,
465 * see <a href="http://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_.2Fapi.2F0.6.2Fchangesets">OSM API</a>.
466 *
467 * Example for an query string:<br>
468 * <pre>
469 * uid=1234&amp;open=true
470 * </pre>
471 *
472 * @param query the query string. If null, an empty query (identical to a query for all changesets) is
473 * assumed
474 * @return the changeset query
475 * @throws ChangesetQueryUrlException if the query string doesn't represent a legal query for changesets
476 */
477 public ChangesetQuery parse(String query) throws ChangesetQueryUrlException {
478 if (query == null)
479 return new ChangesetQuery();
480 query = query.trim();
481 if (query.isEmpty())
482 return new ChangesetQuery();
483 Map<String,String> queryParams = createMapFromQueryString(query);
484 return createFromMap(queryParams);
485 }
486 }
487}
Note: See TracBrowser for help on using the repository browser.