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}