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}