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