001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.UnsupportedEncodingException; 007import java.net.URLEncoder; 008import java.text.DateFormat; 009import java.text.MessageFormat; 010import java.text.ParseException; 011import java.text.SimpleDateFormat; 012import java.util.Arrays; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.Date; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.Map; 019import java.util.Map.Entry; 020 021import org.openstreetmap.josm.data.Bounds; 022import org.openstreetmap.josm.data.coor.LatLon; 023import org.openstreetmap.josm.tools.CheckParameterUtil; 024import org.openstreetmap.josm.tools.Utils; 025 026public class ChangesetQuery { 027 028 /** 029 * Replies a changeset query object from the query part of a OSM API URL for querying 030 * changesets. 031 * 032 * @param query the query part 033 * @return the query object 034 * @throws ChangesetQueryUrlException thrown if query doesn't consist of valid query parameters 035 * 036 */ 037 static public ChangesetQuery buildFromUrlQuery(String query) throws ChangesetQueryUrlException{ 038 return new ChangesetQueryUrlParser().parse(query); 039 } 040 041 /** the user id this query is restricted to. null, if no restriction to a user id applies */ 042 private Integer uid = null; 043 /** the user name this query is restricted to. null, if no restriction to a user name applies */ 044 private String userName = null; 045 /** the bounding box this query is restricted to. null, if no restriction to a bounding box applies */ 046 private Bounds bounds = null; 047 048 private Date closedAfter = null; 049 private Date createdBefore = null; 050 /** indicates whether only open changesets are queried. null, if no restrictions regarding open changesets apply */ 051 private Boolean open = null; 052 /** indicates whether only closed changesets are queried. null, if no restrictions regarding open changesets apply */ 053 private Boolean closed = null; 054 /** a collection of changeset ids to query for */ 055 private Collection<Long> changesetIds = null; 056 057 public ChangesetQuery() {} 058 059 /** 060 * Restricts the query to changesets owned by the user with id <code>uid</code>. 061 * 062 * @param uid the uid of the user. >0 expected. 063 * @return the query object with the applied restriction 064 * @throws IllegalArgumentException thrown if uid <= 0 065 * @see #forUser(String) 066 */ 067 public ChangesetQuery forUser(int uid) throws IllegalArgumentException{ 068 if (uid <= 0) 069 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected. Got ''{1}''.", "uid", uid)); 070 this.uid = uid; 071 this.userName = null; 072 return this; 073 } 074 075 /** 076 * Restricts the query to changesets owned by the user with user name <code>username</code>. 077 * 078 * Caveat: for historical reasons the username might not be unique! It is recommended to use 079 * {@link #forUser(int)} to restrict the query to a specific user. 080 * 081 * @param username the username. Must not be null. 082 * @return the query object with the applied restriction 083 * @throws IllegalArgumentException thrown if username is null. 084 * @see #forUser(int) 085 */ 086 public ChangesetQuery forUser(String username) { 087 CheckParameterUtil.ensureParameterNotNull(username, "username"); 088 this.userName = username; 089 this.uid = null; 090 return this; 091 } 092 093 /** 094 * Replies true if this query is restricted to user whom we only know the user name 095 * for. 096 * 097 * @return true if this query is restricted to user whom we only know the user name 098 * for 099 */ 100 public boolean isRestrictedToPartiallyIdentifiedUser() { 101 return userName != null; 102 } 103 104 /** 105 * Replies the user name which this query is restricted to. null, if this query isn't 106 * restricted to a user name, i.e. if {@link #isRestrictedToPartiallyIdentifiedUser()} is false. 107 * 108 * @return the user name which this query is restricted to 109 */ 110 public String getUserName() { 111 return userName; 112 } 113 114 /** 115 * Replies true if this query is restricted to user whom know the user id for. 116 * 117 * @return true if this query is restricted to user whom know the user id for 118 */ 119 public boolean isRestrictedToFullyIdentifiedUser() { 120 return uid > 0; 121 } 122 123 /** 124 * Replies a query which is restricted to a bounding box. 125 * 126 * @param minLon min longitude of the bounding box. Valid longitude value expected. 127 * @param minLat min latitude of the bounding box. Valid latitude value expected. 128 * @param maxLon max longitude of the bounding box. Valid longitude value expected. 129 * @param maxLat max latitude of the bounding box. Valid latitude value expected. 130 * 131 * @return the restricted changeset query 132 * @throws IllegalArgumentException thrown if either of the parameters isn't a valid longitude or 133 * latitude value 134 */ 135 public ChangesetQuery inBbox(double minLon, double minLat, double maxLon, double maxLat) throws IllegalArgumentException{ 136 if (!LatLon.isValidLon(minLon)) 137 throw new IllegalArgumentException(tr("Illegal longitude value for parameter ''{0}'', got {1}", "minLon", minLon)); 138 if (!LatLon.isValidLon(maxLon)) 139 throw new IllegalArgumentException(tr("Illegal longitude value for parameter ''{0}'', got {1}", "maxLon", maxLon)); 140 if (!LatLon.isValidLat(minLat)) 141 throw new IllegalArgumentException(tr("Illegal latitude value for parameter ''{0}'', got {1}", "minLat", minLat)); 142 if (!LatLon.isValidLat(maxLat)) 143 throw new IllegalArgumentException(tr("Illegal longitude value for parameter ''{0}'', got {1}", "maxLat", maxLat)); 144 145 return inBbox(new LatLon(minLon, minLat), new LatLon(maxLon, maxLat)); 146 } 147 148 /** 149 * Replies a query which is restricted to a bounding box. 150 * 151 * @param min the min lat/lon coordinates of the bounding box. Must not be null. 152 * @param max the max lat/lon coordiantes of the bounding box. Must not be null. 153 * 154 * @return the restricted changeset query 155 * @throws IllegalArgumentException thrown if min is null 156 * @throws IllegalArgumentException thrown if max is null 157 */ 158 public ChangesetQuery inBbox(LatLon min, LatLon max) { 159 CheckParameterUtil.ensureParameterNotNull(min, "min"); 160 CheckParameterUtil.ensureParameterNotNull(max, "max"); 161 this.bounds = new Bounds(min,max); 162 return this; 163 } 164 165 /** 166 * Replies a query which is restricted to a bounding box given by <code>bbox</code>. 167 * 168 * @param bbox the bounding box. Must not be null. 169 * @return the changeset query 170 * @throws IllegalArgumentException thrown if bbox is null. 171 */ 172 public ChangesetQuery inBbox(Bounds bbox) throws IllegalArgumentException { 173 CheckParameterUtil.ensureParameterNotNull(bbox, "bbox"); 174 this.bounds = bbox; 175 return this; 176 } 177 178 /** 179 * Restricts the result to changesets which have been closed after the date given by <code>d</code>. 180 * <code>d</code> d is a date relative to the current time zone. 181 * 182 * @param d the date . Must not be null. 183 * @return the restricted changeset query 184 * @throws IllegalArgumentException thrown if d is null 185 */ 186 public ChangesetQuery closedAfter(Date d) throws IllegalArgumentException{ 187 CheckParameterUtil.ensureParameterNotNull(d, "d"); 188 this.closedAfter = d; 189 return this; 190 } 191 192 /** 193 * Restricts the result to changesets which have been closed after <code>closedAfter</code> and which 194 * habe been created before <code>createdBefore</code>. Both dates are expressed relative to the current 195 * time zone. 196 * 197 * @param closedAfter only reply changesets closed after this date. Must not be null. 198 * @param createdBefore only reply changesets created before this date. Must not be null. 199 * @return the restricted changeset query 200 * @throws IllegalArgumentException thrown if closedAfter is null 201 * @throws IllegalArgumentException thrown if createdBefore is null 202 */ 203 public ChangesetQuery closedAfterAndCreatedBefore(Date closedAfter, Date createdBefore ) throws IllegalArgumentException{ 204 CheckParameterUtil.ensureParameterNotNull(closedAfter, "closedAfter"); 205 CheckParameterUtil.ensureParameterNotNull(createdBefore, "createdBefore"); 206 this.closedAfter = closedAfter; 207 this.createdBefore = createdBefore; 208 return this; 209 } 210 211 /** 212 * Restricts the result to changesets which are or aren't open, depending on the value of 213 * <code>isOpen</code> 214 * 215 * @param isOpen whether changesets should or should not be open 216 * @return the restricted changeset query 217 */ 218 public ChangesetQuery beingOpen(boolean isOpen) { 219 this.open = isOpen; 220 return this; 221 } 222 223 /** 224 * Restricts the result to changesets which are or aren't closed, depending on the value of 225 * <code>isClosed</code> 226 * 227 * @param isClosed whether changesets should or should not be open 228 * @return the restricted changeset query 229 */ 230 public ChangesetQuery beingClosed(boolean isClosed) { 231 this.closed = isClosed; 232 return this; 233 } 234 235 /** 236 * Restricts the query to the given changeset ids (which are added to previously added ones). 237 * 238 * @param changesetIds the changeset ids 239 * @return the query object with the applied restriction 240 * @throws IllegalArgumentException thrown if changesetIds is null. 241 */ 242 public ChangesetQuery forChangesetIds(Collection<Long> changesetIds) { 243 CheckParameterUtil.ensureParameterNotNull(changesetIds, "changesetIds"); 244 this.changesetIds = changesetIds; 245 return this; 246 } 247 248 /** 249 * Replies the query string to be used in a query URL for the OSM API. 250 * 251 * @return the query string 252 */ 253 public String getQueryString() { 254 StringBuffer sb = new StringBuffer(); 255 if (uid != null) { 256 sb.append("user").append("=").append(uid); 257 } else if (userName != null) { 258 try { 259 sb.append("display_name").append("=").append(URLEncoder.encode(userName, "UTF-8")); 260 } catch (UnsupportedEncodingException e) { 261 e.printStackTrace(); 262 } 263 } 264 if (bounds != null) { 265 if (sb.length() > 0) { 266 sb.append("&"); 267 } 268 sb.append("bbox=").append(bounds.encodeAsString(",")); 269 } 270 if (closedAfter != null && createdBefore != null) { 271 if (sb.length() > 0) { 272 sb.append("&"); 273 } 274 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"); 275 sb.append("time").append("=").append(df.format(closedAfter)); 276 sb.append(",").append(df.format(createdBefore)); 277 } else if (closedAfter != null) { 278 if (sb.length() > 0) { 279 sb.append("&"); 280 } 281 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"); 282 sb.append("time").append("=").append(df.format(closedAfter)); 283 } 284 285 if (open != null) { 286 if (sb.length() > 0) { 287 sb.append("&"); 288 } 289 sb.append("open=").append(Boolean.toString(open)); 290 } else if (closed != null) { 291 if (sb.length() > 0) { 292 sb.append("&"); 293 } 294 sb.append("closed=").append(Boolean.toString(closed)); 295 } else if (changesetIds != null) { 296 // since 2013-12-05, see https://github.com/openstreetmap/openstreetmap-website/commit/1d1f194d598e54a5d6fb4f38fb569d4138af0dc8 297 if (sb.length() > 0) { 298 sb.append("&"); 299 } 300 sb.append("changesets=").append(Utils.join(",", changesetIds)); 301 } 302 return sb.toString(); 303 } 304 305 @Override 306 public String toString() { 307 return getQueryString(); 308 } 309 310 public static class ChangesetQueryUrlException extends Exception { 311 312 public ChangesetQueryUrlException() { 313 super(); 314 } 315 316 public ChangesetQueryUrlException(String arg0, Throwable arg1) { 317 super(arg0, arg1); 318 } 319 320 public ChangesetQueryUrlException(String arg0) { 321 super(arg0); 322 } 323 324 public ChangesetQueryUrlException(Throwable arg0) { 325 super(arg0); 326 } 327 } 328 329 public static class ChangesetQueryUrlParser { 330 protected int parseUid(String value) throws ChangesetQueryUrlException { 331 if (value == null || value.trim().isEmpty()) 332 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "uid",value)); 333 int id; 334 try { 335 id = Integer.parseInt(value); 336 if (id <= 0) 337 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "uid",value)); 338 } catch(NumberFormatException e) { 339 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "uid",value)); 340 } 341 return id; 342 } 343 344 protected boolean parseOpen(String value) throws ChangesetQueryUrlException { 345 if (value == null || value.trim().isEmpty()) 346 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "open",value)); 347 if (value.equals("true")) 348 return true; 349 else if (value.equals("false")) 350 return false; 351 else 352 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "open",value)); 353 } 354 355 protected boolean parseBoolean(String value, String parameter) throws ChangesetQueryUrlException { 356 if (value == null || value.trim().isEmpty()) 357 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter,value)); 358 if (value.equals("true")) 359 return true; 360 else if (value.equals("false")) 361 return false; 362 else 363 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter,value)); 364 } 365 366 protected Date parseDate(String value, String parameter) throws ChangesetQueryUrlException { 367 if (value == null || value.trim().isEmpty()) 368 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter,value)); 369 if (value.endsWith("Z")) { 370 // OSM API generates date strings we time zone abbreviation "Z" which Java SimpleDateFormat 371 // doesn't understand. Convert into GMT time zone before parsing. 372 // 373 value = value.substring(0,value.length() - 1) + "GMT+00:00"; 374 } 375 DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"); 376 try { 377 return formatter.parse(value); 378 } catch(ParseException e) { 379 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter,value)); 380 } 381 } 382 383 protected Date[] parseTime(String value) throws ChangesetQueryUrlException { 384 String[] dates = value.split(","); 385 if (dates == null || dates.length == 0 || dates.length > 2) 386 throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "time", value)); 387 if (dates.length == 1) 388 return new Date[]{parseDate(dates[0], "time")}; 389 else if (dates.length == 2) 390 return new Date[]{parseDate(dates[0], "time"),parseDate(dates[1], "time")}; 391 return null; 392 } 393 394 protected Collection<Long> parseLongs(String value) { 395 return value == null || value.isEmpty() 396 ? Collections.<Long>emptySet() : 397 new HashSet<Long>(Utils.transform(Arrays.asList(value.split(",")), new Utils.Function<String, Long>() { 398 @Override 399 public Long apply(String x) { 400 return Long.valueOf(x); 401 } 402 })); 403 } 404 405 protected ChangesetQuery createFromMap(Map<String, String> queryParams) throws ChangesetQueryUrlException { 406 ChangesetQuery csQuery = new ChangesetQuery(); 407 408 for (Entry<String, String> entry: queryParams.entrySet()) { 409 String k = entry.getKey(); 410 if (k.equals("uid")) { 411 if (queryParams.containsKey("display_name")) 412 throw new ChangesetQueryUrlException(tr("Cannot create a changeset query including both the query parameters ''uid'' and ''display_name''")); 413 csQuery.forUser(parseUid(queryParams.get("uid"))); 414 } else if (k.equals("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 } else if (k.equals("open")) { 419 boolean b = parseBoolean(entry.getValue(), "open"); 420 csQuery.beingOpen(b); 421 } else if (k.equals("closed")) { 422 boolean b = parseBoolean(entry.getValue(), "closed"); 423 csQuery.beingClosed(b); 424 } else if (k.equals("time")) { 425 Date[] dates = parseTime(entry.getValue()); 426 switch(dates.length) { 427 case 1: 428 csQuery.closedAfter(dates[0]); 429 break; 430 case 2: 431 csQuery.closedAfterAndCreatedBefore(dates[0], dates[1]); 432 break; 433 } 434 } else if (k.equals("bbox")) { 435 try { 436 csQuery.inBbox(new Bounds(entry.getValue(), ",")); 437 } catch(IllegalArgumentException e) { 438 throw new ChangesetQueryUrlException(e); 439 } 440 } else if (k.equals("changesets")) { 441 try { 442 csQuery.forChangesetIds(parseLongs(entry.getValue())); 443 } catch (NumberFormatException e) { 444 throw new ChangesetQueryUrlException(e); 445 } 446 } else 447 throw new ChangesetQueryUrlException(tr("Unsupported parameter ''{0}'' in changeset query string", k)); 448 } 449 return csQuery; 450 } 451 452 protected Map<String,String> createMapFromQueryString(String query) { 453 Map<String,String> queryParams = new HashMap<String, String>(); 454 String[] keyValuePairs = query.split("&"); 455 for (String keyValuePair: keyValuePairs) { 456 String[] kv = keyValuePair.split("="); 457 queryParams.put(kv[0], kv.length > 1 ? kv[1] : ""); 458 } 459 return queryParams; 460 } 461 462 /** 463 * Parses the changeset query given as URL query parameters and replies a 464 * {@link ChangesetQuery} 465 * 466 * <code>query</code> is the query part of a API url for querying changesets, 467 * see <a href="http://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_.2Fapi.2F0.6.2Fchangesets">OSM API</a>. 468 * 469 * Example for an query string:<br> 470 * <pre> 471 * uid=1234&open=true 472 * </pre> 473 * 474 * @param query the query string. If null, an empty query (identical to a query for all changesets) is 475 * assumed 476 * @return the changeset query 477 * @throws ChangesetQueryUrlException if the query string doesn't represent a legal query for changesets 478 */ 479 public ChangesetQuery parse(String query) throws ChangesetQueryUrlException{ 480 if (query == null) 481 return new ChangesetQuery(); 482 query = query.trim(); 483 if (query.isEmpty()) 484 return new ChangesetQuery(); 485 Map<String,String> queryParams = createMapFromQueryString(query); 486 return createFromMap(queryParams); 487 } 488 } 489}