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 a property to the order by.
153   */
154  public void add(Property p) {
155    list.add(p);
156  }
157
158  public String toString() {
159    return list.toString();
160  }
161
162  /**
163   * Returns the OrderBy in string format.
164   */
165  public String toStringFormat() {
166    if (list.isEmpty()) {
167      return null;
168    }
169    StringBuilder sb = new StringBuilder();
170    for (int i = 0; i < list.size(); i++) {
171      Property property = list.get(i);
172      if (i > 0) {
173        sb.append(", ");
174      }
175      sb.append(property.toStringFormat());
176    }
177    return sb.toString();
178  }
179
180  @Override
181  public boolean equals(Object obj) {
182    if (obj == this) {
183      return true;
184    }
185    if (!(obj instanceof OrderBy<?>)) {
186      return false;
187    }
188    
189    OrderBy<?> e = (OrderBy<?>) obj;
190    return e.list.equals(list);
191  }
192
193  /**
194   * Return a hash value for this OrderBy. This can be to determine logical
195   * equality for OrderBy clauses.
196   */
197  public int hashCode() {
198    return list.hashCode();
199  }
200
201  /**
202   * Clear the orderBy removing any current order by properties.
203   * <p>
204   * This is intended to be used when some code creates a query with a
205   * 'default' order by clause and some other code may clear the 'default'
206   * order by clause and replace.
207   * </p>
208   */
209  public void clear() {
210    list.clear();
211  }
212
213  /**
214   * A property and its ascending descending order.
215   */
216  public static final class Property implements Serializable {
217
218    private static final long serialVersionUID = 1546009780322478077L;
219
220    private String property;
221
222    private boolean ascending;
223
224    public Property(String property, boolean ascending) {
225      this.property = property;
226      this.ascending = ascending;
227    }
228
229    /**
230     * Return a copy of this Property with the path trimmed.
231     */
232    public Property copyWithTrim(String path) {
233      return new Property(property.substring(path.length() + 1), ascending);
234    }
235
236    public int hashCode() {
237      int hc = property.hashCode();
238      hc = hc * 31 + (ascending ? 0 : 1);
239      return hc;
240    }
241    
242    public boolean equals(Object obj) {
243      if (obj == this) {
244        return true;
245      }
246      if (!(obj instanceof Property)) {
247        return false;
248      }
249      
250      Property e = (Property) obj;
251      return e.ascending == ascending 
252          && e.property.equals(property);
253    }
254
255    public String toString() {
256      return toStringFormat();
257    }
258
259    public String toStringFormat() {
260      if (ascending) {
261        return property;
262      } else {
263        return property + " desc";
264      }
265    }
266
267    /**
268     * Reverse the ascending/descending order for this property.
269     */
270    public void reverse() {
271      this.ascending = !ascending;
272    }
273
274    /**
275     * Trim off the pathPrefix.
276     */
277    public void trim(String pathPrefix) {
278      property = property.substring(pathPrefix.length() + 1);
279    }
280
281    /**
282     * Return a copy of this property.
283     */
284    public Property copy() {
285      return new Property(property, ascending);
286    }
287
288    /**
289     * Return the property name.
290     */
291    public String getProperty() {
292      return property;
293    }
294
295    /**
296     * Set the property name.
297     */
298    public void setProperty(String property) {
299      this.property = property;
300    }
301
302    /**
303     * Return true if the order is ascending.
304     */
305    public boolean isAscending() {
306      return ascending;
307    }
308
309    /**
310     * Set to true if the order is ascending.
311     */
312    public void setAscending(boolean ascending) {
313      this.ascending = ascending;
314    }
315
316  }
317
318  private void parse(String orderByClause) {
319
320    if (orderByClause == null) {
321      return;
322    }
323
324    String[] chunks = orderByClause.split(",");
325    for (int i = 0; i < chunks.length; i++) {
326
327      String[] pairs = chunks[i].split(" ");
328      Property p = parseProperty(pairs);
329      if (p != null) {
330        list.add(p);
331      }
332    }
333  }
334
335  private Property parseProperty(String[] pairs) {
336    if (pairs.length == 0) {
337      return null;
338    }
339
340    ArrayList<String> wordList = new ArrayList<String>(pairs.length);
341    for (int i = 0; i < pairs.length; i++) {
342      if (!isEmptyString(pairs[i])) {
343        wordList.add(pairs[i]);
344      }
345    }
346    if (wordList.isEmpty()) {
347      return null;
348    }
349    if (wordList.size() == 1) {
350      return new Property(wordList.get(0), true);
351    }
352    if (wordList.size() == 2) {
353      boolean asc = isAscending(wordList.get(1));
354      return new Property(wordList.get(0), asc);
355    }
356    String m = "Expecting a max of 2 words in [" + Arrays.toString(pairs)
357        + "] but got " + wordList.size();
358    throw new RuntimeException(m);
359  }
360
361  private boolean isAscending(String s) {
362    s = s.toLowerCase();
363    if (s.startsWith("asc")) {
364      return true;
365    }
366    if (s.startsWith("desc")) {
367      return false;
368    }
369    String m = "Expecting [" + s + "] to be asc or desc?";
370    throw new RuntimeException(m);
371  }
372
373  private boolean isEmptyString(String s) {
374    return s == null || s.length() == 0;
375  }
376}