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