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}