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