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

Last change on this file since 12187 was 11878, checked in by Don-vip, 7 years ago

findbugs - EI_EXPOSE_REP2 + javadoc

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