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