001package io.ebean; 002 003import java.io.Serializable; 004import java.util.ArrayList; 005import java.util.List; 006import java.util.Objects; 007 008/** 009 * Represents an Order By for a Query. 010 * <p> 011 * Is a ordered list of OrderBy.Property objects each specifying a property and 012 * whether it is ascending or descending order. 013 * </p> 014 * <p> 015 * Typically you will not construct an OrderBy yourself but use one that exists 016 * on the Query object. 017 * </p> 018 */ 019public final class OrderBy<T> implements Serializable { 020 021 private static final long serialVersionUID = 9157089257745730539L; 022 023 private transient Query<T> query; 024 025 private final List<Property> list; 026 027 /** 028 * Create an empty OrderBy with no associated query. 029 */ 030 public OrderBy() { 031 this.list = new ArrayList<>(3); 032 } 033 034 private OrderBy(List<Property> list) { 035 this.list = list; 036 } 037 038 /** 039 * Create an orderBy parsing the order by clause. 040 * <p> 041 * The order by clause follows SQL order by clause with comma's between each 042 * property and optionally "asc" or "desc" to represent ascending or 043 * descending order respectively. 044 * </p> 045 */ 046 public OrderBy(String orderByClause) { 047 this(null, orderByClause); 048 } 049 050 /** 051 * Construct with a given query and order by clause. 052 */ 053 public OrderBy(Query<T> query, String orderByClause) { 054 this.query = query; 055 this.list = new ArrayList<>(3); 056 parse(orderByClause); 057 } 058 059 /** 060 * Reverse the ascending/descending order on all the properties. 061 */ 062 public void reverse() { 063 for (Property aList : list) { 064 aList.reverse(); 065 } 066 } 067 068 /** 069 * Add a property with ascending order to this OrderBy. 070 */ 071 public Query<T> asc(String propertyName) { 072 073 list.add(new Property(propertyName, true)); 074 return query; 075 } 076 077 /** 078 * Add a property with ascending order to this OrderBy. 079 */ 080 public Query<T> asc(String propertyName, String collation) { 081 list.add(new Property(propertyName, true, collation)); 082 return query; 083 } 084 085 /** 086 * Add a property with descending order to this OrderBy. 087 */ 088 public Query<T> desc(String propertyName) { 089 list.add(new Property(propertyName, false)); 090 return query; 091 } 092 093 /** 094 * Add a property with descending order to this OrderBy. 095 */ 096 public Query<T> desc(String propertyName, String collation) { 097 list.add(new Property(propertyName, false, collation)); 098 return query; 099 } 100 101 102 /** 103 * Return true if the property is known to be contained in the order by clause. 104 */ 105 public boolean containsProperty(String propertyName) { 106 for (Property aList : list) { 107 if (propertyName.equals(aList.getProperty())) { 108 return true; 109 } 110 } 111 return false; 112 } 113 114 /** 115 * Return a copy of this OrderBy with the path trimmed. 116 */ 117 public OrderBy<T> copyWithTrim(String path) { 118 List<Property> newList = new ArrayList<>(list.size()); 119 for (Property aList : list) { 120 newList.add(aList.copyWithTrim(path)); 121 } 122 return new OrderBy<>(newList); 123 } 124 125 /** 126 * Return the properties for this OrderBy. 127 */ 128 public List<Property> getProperties() { 129 // not returning an Immutable list at this point 130 return list; 131 } 132 133 /** 134 * Return true if this OrderBy does not have any properties. 135 */ 136 public boolean isEmpty() { 137 return list.isEmpty(); 138 } 139 140 /** 141 * Return the associated query if there is one. 142 */ 143 public Query<T> getQuery() { 144 return query; 145 } 146 147 /** 148 * Associate this OrderBy with a query. 149 */ 150 public void setQuery(Query<T> query) { 151 this.query = query; 152 } 153 154 /** 155 * Return a copy of the OrderBy. 156 */ 157 public OrderBy<T> copy() { 158 OrderBy<T> copy = new OrderBy<>(); 159 for (Property property : list) { 160 copy.add(property.copy()); 161 } 162 return copy; 163 } 164 165 /** 166 * Add to the order by by parsing a raw expression. 167 */ 168 public void add(String rawExpression) { 169 parse(rawExpression); 170 } 171 172 /** 173 * Add a property to the order by. 174 */ 175 public void add(Property p) { 176 list.add(p); 177 } 178 179 @Override 180 public String toString() { 181 return list.toString(); 182 } 183 184 /** 185 * Returns the OrderBy in string format. 186 */ 187 public String toStringFormat() { 188 if (list.isEmpty()) { 189 return null; 190 } 191 StringBuilder sb = new StringBuilder(); 192 for (int i = 0; i < list.size(); i++) { 193 Property property = list.get(i); 194 if (i > 0) { 195 sb.append(", "); 196 } 197 sb.append(property.toStringFormat()); 198 } 199 return sb.toString(); 200 } 201 202 @Override 203 public boolean equals(Object obj) { 204 if (obj == this) { 205 return true; 206 } 207 if (!(obj instanceof OrderBy<?>)) { 208 return false; 209 } 210 211 OrderBy<?> e = (OrderBy<?>) obj; 212 return e.list.equals(list); 213 } 214 215 /** 216 * Return a hash value for this OrderBy. This can be to determine logical 217 * equality for OrderBy clauses. 218 */ 219 @Override 220 public int hashCode() { 221 return list.hashCode(); 222 } 223 224 /** 225 * Clear the orderBy removing any current order by properties. 226 * <p> 227 * This is intended to be used when some code creates a query with a 228 * 'default' order by clause and some other code may clear the 'default' 229 * order by clause and replace. 230 * </p> 231 */ 232 public OrderBy<T> clear() { 233 list.clear(); 234 return this; 235 } 236 237 /** 238 * Return true if this order by can be used in select clause. 239 */ 240 public boolean supportsSelect() { 241 for (Property property : list) { 242 if (!property.supportsSelect()) { 243 return false; 244 } 245 } 246 return true; 247 } 248 249 /** 250 * A property and its ascending descending order. 251 */ 252 public static final class Property implements Serializable { 253 254 private static final long serialVersionUID = 1546009780322478077L; 255 256 private String property; 257 258 private boolean ascending; 259 260 private String collation; 261 262 private String nulls; 263 264 private String highLow; 265 266 public Property(String property, boolean ascending) { 267 this.property = property; 268 this.ascending = ascending; 269 } 270 271 public Property(String property, boolean ascending, String nulls, String highLow) { 272 this.property = property; 273 this.ascending = ascending; 274 this.nulls = nulls; 275 this.highLow = highLow; 276 } 277 278 public Property(String property, boolean ascending, String collation) { 279 this.property = property; 280 this.ascending = ascending; 281 this.collation = collation; 282 } 283 284 public Property(String property, boolean ascending, String collation, String nulls, String highLow) { 285 this.property = property; 286 this.ascending = ascending; 287 this.collation = collation; 288 this.nulls = nulls; 289 this.highLow = highLow; 290 } 291 292 /** 293 * Return a copy of this Property with the path trimmed. 294 */ 295 public Property copyWithTrim(String path) { 296 return new Property(property.substring(path.length() + 1), ascending, collation, nulls, highLow); 297 } 298 299 @Override 300 public int hashCode() { 301 int hc = property.hashCode(); 302 hc = hc * 92821 + (ascending ? 0 : 1); 303 hc = hc * 92821 + (collation == null ? 0 : collation.hashCode()); 304 hc = hc * 92821 + (nulls == null ? 0 : nulls.hashCode()); 305 hc = hc * 92821 + (highLow == null ? 0 : highLow.hashCode()); 306 return hc; 307 } 308 309 @Override 310 public boolean equals(Object obj) { 311 if (obj == this) { 312 return true; 313 } 314 if (!(obj instanceof Property)) { 315 return false; 316 } 317 Property e = (Property) obj; 318 if (ascending != e.ascending) return false; 319 if (!property.equals(e.property)) return false; 320 if (!Objects.equals(collation, e.collation)) return false; 321 if (!Objects.equals(nulls, e.nulls)) return false; 322 return Objects.equals(highLow, e.highLow); 323 } 324 325 @Override 326 public String toString() { 327 return toStringFormat(); 328 } 329 330 public String toStringFormat() { 331 if (nulls == null && collation == null) { 332 if (ascending) { 333 return property; 334 } else { 335 return property + " desc"; 336 } 337 } else { 338 StringBuilder sb = new StringBuilder(); 339 if (collation != null) { 340 if (collation.contains("${}")) { 341 // this is a complex collation, e.g. DB2 - we must replace the property 342 sb.append(collation.replace("${}", property)); 343 } else { 344 sb.append(property); 345 sb.append(" collate ").append(collation); 346 } 347 } else { 348 sb.append(property); 349 } 350 if (!ascending) { 351 sb.append(" ").append("desc"); 352 } 353 if (nulls != null) { 354 sb.append(" ").append(nulls).append(" ").append(highLow); 355 } 356 return sb.toString(); 357 } 358 } 359 360 /** 361 * Reverse the ascending/descending order for this property. 362 */ 363 public void reverse() { 364 this.ascending = !ascending; 365 } 366 367 /** 368 * Trim off the pathPrefix. 369 */ 370 public void trim(String pathPrefix) { 371 property = property.substring(pathPrefix.length() + 1); 372 } 373 374 /** 375 * Return a copy of this property. 376 */ 377 public Property copy() { 378 return new Property(property, ascending, collation, nulls, highLow); 379 } 380 381 /** 382 * Return the property name. 383 */ 384 public String getProperty() { 385 return property; 386 } 387 388 /** 389 * Set the property name. 390 */ 391 public void setProperty(String property) { 392 this.property = property; 393 } 394 395 /** 396 * Return true if the order is ascending. 397 */ 398 public boolean isAscending() { 399 return ascending; 400 } 401 402 /** 403 * Set to true if the order is ascending. 404 */ 405 public void setAscending(boolean ascending) { 406 this.ascending = ascending; 407 } 408 409 /** 410 * Support use in select clause if no collation or nulls ordering. 411 */ 412 boolean supportsSelect() { 413 return nulls == null; 414 } 415 } 416 417 private void parse(String orderByClause) { 418 419 if (orderByClause == null) { 420 return; 421 } 422 423 String[] chunks = orderByClause.split(","); 424 for (String chunk : chunks) { 425 Property p = parseProperty(chunk); 426 if (p != null) { 427 list.add(p); 428 } 429 } 430 } 431 432 private Property parseProperty(String chunk) { 433 String[] pairs = chunk.split(" "); 434 if (pairs.length == 0) { 435 return null; 436 } 437 438 ArrayList<String> wordList = new ArrayList<>(pairs.length); 439 for (String pair : pairs) { 440 if (!isEmptyString(pair)) { 441 wordList.add(pair); 442 } 443 } 444 if (wordList.isEmpty()) { 445 return null; 446 } 447 if (wordList.size() == 1) { 448 return new Property(wordList.get(0), true); 449 } 450 if (wordList.size() == 2) { 451 boolean asc = isAscending(wordList.get(1)); 452 return new Property(wordList.get(0), asc); 453 } 454 if (wordList.size() == 4) { 455 // nulls high or nulls low as 3rd and 4th 456 boolean asc = isAscending(wordList.get(1)); 457 return new Property(wordList.get(0), asc, wordList.get(2), wordList.get(3)); 458 } 459 return new Property(chunk.trim(), true); 460 } 461 462 private boolean isAscending(String s) { 463 s = s.toLowerCase(); 464 if (s.startsWith("asc")) { 465 return true; 466 } 467 if (s.startsWith("desc")) { 468 return false; 469 } 470 String m = "Expecting [" + s + "] to be asc or desc?"; 471 throw new RuntimeException(m); 472 } 473 474 private boolean isEmptyString(String s) { 475 return s == null || s.isEmpty(); 476 } 477}