001package com.avaje.ebean;
002
003import java.io.Serializable;
004import java.sql.ResultSet;
005import java.util.*;
006
007import com.avaje.ebean.util.CamelCaseHelper;
008
009/**
010 * Used to build object graphs based on a raw SQL statement (rather than
011 * generated by Ebean).
012 * <p>
013 * If you don't want to build object graphs you can use {@link SqlQuery} instead
014 * which returns {@link SqlRow} objects rather than entity beans.
015 * </p>
016 * <p>
017 * <b>Unparsed RawSql:</b>
018 * </p>
019 * <p>
020 * When RawSql is created via {@link RawSqlBuilder#unparsed(String)} then Ebean can not
021 * modify the SQL at all. It can't add any extra expressions into the SQL.
022 * </p>
023 * <p>
024 * <b>Parsed RawSql:</b>
025 * </p>
026 * <p>
027 * When RawSql is created via {@link RawSqlBuilder#parse(String)} then Ebean will parse the
028 * SQL and find places in the SQL where it can add extra where expressions, add
029 * extra having expressions or replace the order by clause. If you want to
030 * explicitly tell Ebean where these insertion points are you can place special
031 * strings into your SQL ({@code ${where}} or {@code ${andWhere}} and {@code ${having}} or
032 * {@code ${andHaving})}.
033 * </p>
034 * <p>
035 * If the SQL already includes a WHERE clause put in {@code ${andWhere}} in the location
036 * you want Ebean to add any extra where expressions. If the SQL doesn't have a
037 * WHERE clause put {@code ${where}} in instead. Similarly you can put in {@code ${having}} or
038 * {@code ${andHaving}} where you want Ebean put add extra having expressions.
039 * </p>
040 * <p>
041 * <b>Aggregates:</b>
042 * </p>
043 * <p>
044 * Often RawSql will be used with Aggregate functions (sum, avg, max etc). The
045 * follow example shows an example based on Total Order Amount -
046 * sum(d.order_qty*d.unit_price).
047 * </p>
048 * <p>
049 * We can use a OrderAggregate bean that has a &#064;Sql to indicate it is based
050 * on RawSql and not based on a real DB Table or DB View. It has some properties
051 * to hold the values for the aggregate functions (sum etc) and a &#064;OneToOne
052 * to Order.
053 * </p>
054 *
055 * <h3>Example OrderAggregate</h3>
056 * 
057 * <pre>{@code
058 *  ...
059 *  // @Sql indicates to that this bean
060 *  // is based on RawSql rather than a table
061 * 
062 * @Entity
063 * @Sql
064 * public class OrderAggregate {
065 * 
066 *  @OneToOne
067 *  Order order;
068 *      
069 *  Double totalAmount;
070 *  
071 *  Double totalItems;
072 *  
073 *  // getters and setters
074 *  ...
075 *
076 * }</pre>
077 *
078 * <h3>Example 1:</h3>
079 * 
080 * <pre>{@code
081 *
082 *   String sql = " select order_id, o.status, c.id, c.name, sum(d.order_qty*d.unit_price) as totalAmount"
083 *     + " from o_order o"
084 *     + " join o_customer c on c.id = o.kcustomer_id "
085 *     + " join o_order_detail d on d.order_id = o.id " + " group by order_id, o.status ";
086 * 
087 *   RawSql rawSql = RawSqlBuilder.parse(sql)
088 *     // map the sql result columns to bean properties
089 *     .columnMapping("order_id", "order.id")
090 *     .columnMapping("o.status", "order.status")
091 *     .columnMapping("c.id", "order.customer.id")
092 *     .columnMapping("c.name", "order.customer.name")
093 *     // we don't need to map this one due to the sql column alias
094 *     // .columnMapping("sum(d.order_qty*d.unit_price)", "totalAmount")
095 *     .create();
096 * 
097 *   List<OrderAggregate> list = Ebean.find(OrderAggregate.class)
098 *       .setRawSql(rawSql)
099 *       .where().gt("order.id", 0)
100 *       .having().gt("totalAmount", 20)
101 *       .findList();
102 * 
103 *
104 * }</pre>
105 * 
106 * <h3>Example 2:</h3>
107 * 
108 * <p>
109 * The following example uses a FetchConfig().query() so that after the initial
110 * RawSql query is executed Ebean executes a secondary query to fetch the
111 * associated order status, orderDate along with the customer name.
112 * </p>
113 * 
114 * <pre>{@code
115 *
116 *  String sql = " select order_id, 'ignoreMe', sum(d.order_qty*d.unit_price) as totalAmount "
117 *     + " from o_order_detail d"
118 *     + " group by order_id ";
119 * 
120 *   RawSql rawSql = RawSqlBuilder.parse(sql)
121 *     .columnMapping("order_id", "order.id")
122 *     .columnMappingIgnore("'ignoreMe'")
123 *     .create();
124 * 
125 *   List<OrderAggregate> orders = Ebean.find(OrderAggregate.class)
126 *     .setRawSql(rawSql)
127 *     .fetch("order", "status,orderDate", new FetchConfig().query())
128 *     .fetch("order.customer", "name")
129 *     .where().gt("order.id", 0)
130 *     .having().gt("totalAmount", 20)
131 *     .order().desc("totalAmount")
132 *     .setMaxRows(10)
133 *     .findList();
134 * 
135 * }</pre>
136 *
137 *
138 * <h3>Example 3: tableAliasMapping</h3>
139 * <p>
140 *   Instead of mapping each column you can map each table alias to a path using tableAliasMapping().
141 * </p>
142 * <pre>{@code
143 *
144 *   String rs = "select o.id, o.status, c.id, c.name, "+
145 *               " d.id, d.order_qty, p.id, p.name " +
146 *               "from o_order o join o_customer c on c.id = o.kcustomer_id " +
147 *               "join o_order_detail d on d.order_id = o.id  " +
148 *               "join o_product p on p.id = d.product_id  " +
149 *               "where o.id <= :maxOrderId  and p.id = :productId "+
150 *               "order by o.id, d.id asc";
151 *
152 *  RawSql rawSql = RawSqlBuilder.parse(rs)
153 *       .tableAliasMapping("c", "customer")
154 *       .tableAliasMapping("d", "details")
155 *       .tableAliasMapping("p", "details.product")
156 *       .create();
157 *
158 *  List<Order> ordersFromRaw = Ebean.find(Order.class)
159 *       .setRawSql(rawSql)
160 *       .setParameter("maxOrderId", 2)
161 *       .setParameter("productId", 1)
162 *       .findList();
163 *
164 * }</pre>
165 *
166 *
167 * <p>
168 * Note that lazy loading also works with object graphs built with RawSql.
169 * </p>
170 * 
171 */
172public final class RawSql implements Serializable {
173
174  private static final long serialVersionUID = 1L;
175
176  private final ResultSet resultSet;
177  
178  private final Sql sql;
179
180  private final ColumnMapping columnMapping;
181
182  /**
183   * Construct with a ResultSet and properties that the columns map to.
184   * <p>
185   * The properties listed in the propertyNames must be in the same order as the columns in the
186   * resultSet.
187   * <p>
188   * When a query executes this RawSql object then it will close the resultSet.
189   */
190  public RawSql(ResultSet resultSet, String... propertyNames) {
191    this.resultSet = resultSet;
192    this.sql = null;
193    this.columnMapping = new ColumnMapping(propertyNames);
194  }
195  
196  protected RawSql(ResultSet resultSet, Sql sql, ColumnMapping columnMapping) {
197    this.resultSet = resultSet;
198    this.sql = sql;
199    this.columnMapping = columnMapping;
200  }
201
202  /**
203   * Return the Sql either unparsed or in parsed (broken up) form.
204   */
205  public Sql getSql() {
206    return sql;
207  }
208
209  
210  /**
211   * Return the resultSet if this is a ResultSet based RawSql.
212   */
213  public ResultSet getResultSet() {
214    return resultSet;
215  }
216
217  /**
218   * Return the column mapping for the SQL columns to bean properties.
219   */
220  public ColumnMapping getColumnMapping() {
221    return columnMapping;
222  }
223
224  /**
225   * Return the hash for this query.
226   */
227  public int queryHash() {
228    if (resultSet != null) {
229      return 31 * columnMapping.queryHash();
230    }
231    return 31 * sql.queryHash() + columnMapping.queryHash();
232  }
233
234  /**
235   * Represents the sql part of the query. For parsed RawSql the sql is broken
236   * up so that Ebean can insert extra WHERE and HAVING expressions into the
237   * SQL.
238   */
239  public static final class Sql implements Serializable {
240
241    private static final long serialVersionUID = 1L;
242
243    private final boolean parsed;
244
245    private final String unparsedSql;
246
247    private final String preFrom;
248
249    private final String preWhere;
250
251    private final boolean andWhereExpr;
252
253    private final String preHaving;
254
255    private final boolean andHavingExpr;
256
257    private final String orderByPrefix;
258
259    private final String orderBy;
260
261    private final boolean distinct;
262
263    private final int queryHashCode;
264
265    /**
266     * Construct for unparsed SQL.
267     */
268    protected Sql(String unparsedSql) {
269      this.queryHashCode = unparsedSql.hashCode();
270      this.parsed = false;
271      this.unparsedSql = unparsedSql;
272      this.preFrom = null;
273      this.preHaving = null;
274      this.preWhere = null;
275      this.andHavingExpr = false;
276      this.andWhereExpr = false;
277      this.orderByPrefix = null;
278      this.orderBy = null;
279      this.distinct = false;
280    }
281
282    /**
283     * Construct for parsed SQL.
284     */
285    protected Sql(int queryHashCode, String preFrom, String preWhere, boolean andWhereExpr,
286        String preHaving, boolean andHavingExpr, String orderByPrefix, String orderBy, boolean distinct) {
287
288      this.queryHashCode = queryHashCode;
289      this.parsed = true;
290      this.unparsedSql = null;
291      this.preFrom = preFrom;
292      this.preHaving = preHaving;
293      this.preWhere = preWhere;
294      this.andHavingExpr = andHavingExpr;
295      this.andWhereExpr = andWhereExpr;
296      this.orderByPrefix = orderByPrefix;
297      this.orderBy = orderBy;
298      this.distinct = distinct;
299    }
300
301    /**
302     * Return a hash for this query.
303     */
304    public int queryHash() {
305      return queryHashCode;
306    }
307
308    public String toString() {
309      if (!parsed) {
310        return "unparsed[" + unparsedSql + "]";
311      }
312      return "select[" + preFrom + "] preWhere[" + preWhere + "] preHaving[" + preHaving
313          + "] orderBy[" + orderBy + "]";
314    }
315
316    public boolean isDistinct() {
317      return distinct;
318    }
319
320    /**
321     * Return true if the SQL is left completely unmodified.
322     * <p>
323     * This means Ebean can't add WHERE or HAVING expressions into the query -
324     * it will be left completely unmodified.
325     * </p>
326     */
327    public boolean isParsed() {
328      return parsed;
329    }
330
331    /**
332     * Return the SQL when it is unparsed.
333     */
334    public String getUnparsedSql() {
335      return unparsedSql;
336    }
337
338    /**
339     * Return the SQL prior to FROM clause.
340     */
341    public String getPreFrom() {
342      return preFrom;
343    }
344
345    /**
346     * Return the SQL prior to WHERE clause.
347     */
348    public String getPreWhere() {
349      return preWhere;
350    }
351
352    /**
353     * Return true if there is already a WHERE clause and any extra where
354     * expressions start with AND.
355     */
356    public boolean isAndWhereExpr() {
357      return andWhereExpr;
358    }
359
360    /**
361     * Return the SQL prior to HAVING clause.
362     */
363    public String getPreHaving() {
364      return preHaving;
365    }
366
367    /**
368     * Return true if there is already a HAVING clause and any extra having
369     * expressions start with AND.
370     */
371    public boolean isAndHavingExpr() {
372      return andHavingExpr;
373    }
374
375    /**
376     * Return the 'order by' keywords.
377     * This can contain additional keywords, for example 'order siblings by' as Oracle syntax.
378     */
379    public String getOrderByPrefix() {
380      return (orderByPrefix == null) ? "order by" : orderByPrefix;
381    }
382
383    /**
384     * Return the SQL ORDER BY clause.
385     */
386    public String getOrderBy() {
387      return orderBy;
388    }
389
390  }
391
392  /**
393   * Defines the column mapping for raw sql DB columns to bean properties.
394   */
395  public static final class ColumnMapping implements Serializable {
396
397    private static final long serialVersionUID = 1L;
398
399    private final LinkedHashMap<String, Column> dbColumnMap;
400
401    private final Map<String, String> propertyMap;
402    
403    private final Map<String, Column> propertyColumnMap;
404
405    private final boolean parsed;
406
407    private final boolean immutable;
408
409    private final int queryHashCode;
410
411    /**
412     * Construct from parsed sql where the columns have been identified.
413     */
414    protected ColumnMapping(List<Column> columns) {
415      this.queryHashCode = 0;
416      this.immutable = false;
417      this.parsed = true;
418      this.propertyMap = null;
419      this.propertyColumnMap = null;
420      this.dbColumnMap = new LinkedHashMap<String, Column>();
421      for (int i = 0; i < columns.size(); i++) {
422        Column c = columns.get(i);
423        dbColumnMap.put(c.getDbColumn(), c);
424      }
425    }
426
427    /**
428     * Construct for unparsed sql.
429     */
430    protected ColumnMapping() {
431      this.queryHashCode = 0;
432      this.immutable = false;
433      this.parsed = false;
434      this.propertyMap = null;
435      this.propertyColumnMap = null;
436      this.dbColumnMap = new LinkedHashMap<String, Column>();
437    }
438    
439    /**
440     * Construct for ResultSet use.
441     */
442    protected ColumnMapping(String... propertyNames) {
443      this.immutable = false;
444      this.parsed = false;
445      this.propertyMap = null;
446      //this.propertyColumnMap = null;
447      this.dbColumnMap = new LinkedHashMap<String, Column>();
448      
449      int hc = 31;
450      int pos = 0;
451      for (String prop : propertyNames) {
452        hc = 31 * hc + prop.hashCode();
453        dbColumnMap.put(prop, new Column(pos++, prop, null, prop));
454      }
455      propertyColumnMap = dbColumnMap;
456      this.queryHashCode = hc;
457    }
458
459    /**
460     * Construct an immutable ColumnMapping based on collected information.
461     */
462    protected ColumnMapping(boolean parsed, LinkedHashMap<String, Column> dbColumnMap) {
463      this.immutable = true;
464      this.parsed = parsed;
465      this.dbColumnMap = dbColumnMap;
466
467      int hc = ColumnMapping.class.getName().hashCode();
468
469      HashMap<String, Column> pcMap = new HashMap<String, Column>();
470      HashMap<String, String> pMap = new HashMap<String, String>();
471
472      for (Column c : dbColumnMap.values()) {
473        pMap.put(c.getPropertyName(), c.getDbColumn());
474        pcMap.put(c.getPropertyName(), c);
475        hc = 31 * hc + ((c.getPropertyName() == null) ? 0 : c.getPropertyName().hashCode());
476        hc = 31 * hc + ((c.getDbColumn() == null) ? 0 : c.getDbColumn().hashCode());
477      }
478      this.propertyMap = Collections.unmodifiableMap(pMap);
479      this.propertyColumnMap = Collections.unmodifiableMap(pcMap);
480      this.queryHashCode = hc;
481    }
482
483    /**
484     * Return true if the property is mapped.
485     */
486    public boolean contains(String property) {
487      return this.propertyColumnMap.containsKey(property);
488    }
489
490    /**
491     * Creates an immutable copy of this ColumnMapping.
492     * 
493     * @throws IllegalStateException
494     *           when a propertyName has not been defined for a column.
495     */
496    protected ColumnMapping createImmutableCopy() {
497
498      for (Column c : dbColumnMap.values()) {
499        c.checkMapping();
500      }
501
502      return new ColumnMapping(parsed, dbColumnMap);
503    }
504
505    protected void columnMapping(String dbColumn, String propertyName) {
506
507      if (immutable) {
508        throw new IllegalStateException("Should never happen");
509      }
510      if (!parsed) {
511        int pos = dbColumnMap.size();
512        dbColumnMap.put(dbColumn, new Column(pos, dbColumn, null, propertyName));
513      } else {
514        Column column = dbColumnMap.get(dbColumn);
515        if (column == null) {
516          String msg = "DB Column [" + dbColumn + "] not found in mapping. Expecting one of ["
517              + dbColumnMap.keySet() + "]";
518          throw new IllegalArgumentException(msg);
519        }
520        column.setPropertyName(propertyName);
521      }
522    }
523
524    /**
525     * Return the query hash for this column mapping.
526     */
527    public int queryHash() {
528      if (queryHashCode == 0) {
529        throw new RuntimeException("Bug: queryHashCode == 0");
530      }
531      return queryHashCode;
532    }
533
534    /**
535     * Returns true if the Columns where supplied by parsing the sql select
536     * clause.
537     * <p>
538     * In the case where the columns where parsed then we can do extra checks on
539     * the column mapping such as, is the column a valid one in the sql and
540     * whether all the columns in the sql have been mapped.
541     * </p>
542     */
543    public boolean isParsed() {
544      return parsed;
545    }
546
547    /**
548     * Return the number of columns in this column mapping.
549     */
550    public int size() {
551      return dbColumnMap.size();
552    }
553
554    /**
555     * Return the column mapping.
556     */
557    protected Map<String, Column> mapping() {
558      return dbColumnMap;
559    }
560
561    /**
562     * Return the mapping by DB column.
563     */
564    public Map<String, String> getMapping() {
565      return propertyMap;
566    }
567
568    /**
569     * Return the index position by bean property name.
570     */
571    public int getIndexPosition(String property) {
572      Column c = propertyColumnMap.get(property);
573      return c == null ? -1 : c.getIndexPos();
574    }
575
576    /**
577     * Return an iterator of the Columns.
578     */
579    public Iterator<Column> getColumns() {
580      return dbColumnMap.values().iterator();
581    }
582
583    /**
584     * Modify any column mappings with the given table alias to have the path prefix.
585     * <p>
586     * For example modify all mappings with table alias "c" to have the path prefix "customer".
587     * </p>
588     */
589    public void tableAliasMapping(String tableAlias, String path) {
590
591      String startMatch = tableAlias+".";
592      for (Map.Entry<String, Column> entry : dbColumnMap.entrySet()) {
593        if (entry.getKey().startsWith(startMatch)) {
594          entry.getValue().tableAliasMapping(path);
595        }
596      }
597    }
598
599    /**
600     * A Column of the RawSql that is mapped to a bean property (or ignored).
601     */
602    public static class Column implements Serializable {
603
604      private static final long serialVersionUID = 1L;
605      private final int indexPos;
606      private final String dbColumn;
607
608      private final String dbAlias;
609
610      private String propertyName;
611
612      /**
613       * Construct a Column.
614       */
615      public Column(int indexPos, String dbColumn, String dbAlias) {
616        this(indexPos, dbColumn, dbAlias, derivePropertyName(dbAlias, dbColumn));
617      }
618
619      private Column(int indexPos, String dbColumn, String dbAlias, String propertyName) {
620        this.indexPos = indexPos;
621        this.dbColumn = dbColumn;
622        this.dbAlias = dbAlias;
623        if (propertyName == null && dbAlias != null) {
624          this.propertyName = dbAlias;
625        } else {
626          this.propertyName = propertyName;
627        }
628      }
629
630      private static String derivePropertyName(String dbAlias, String dbColumn) {
631        if (dbAlias != null) {
632          return dbAlias;
633        }
634        int dotPos = dbColumn.indexOf('.');
635        if (dotPos > -1) {
636          dbColumn = dbColumn.substring(dotPos + 1);
637        }
638        return CamelCaseHelper.toCamelFromUnderscore(dbColumn);
639      }
640
641      private void checkMapping() {
642        if (propertyName == null) {
643          String msg = "No propertyName defined (Column mapping) for dbColumn [" + dbColumn + "]";
644          throw new IllegalStateException(msg);
645        }
646      }
647
648      public String toString() {
649        return dbColumn + "->" + propertyName;
650      }
651
652      /**
653       * Return the index position of this column.
654       */
655      public int getIndexPos() {
656        return indexPos;
657      }
658
659      /**
660       * Return the DB column name including table alias (if it has one).
661       */
662      public String getDbColumn() {
663        return dbColumn;
664      }
665
666      /**
667       * Return the DB column alias (if it has one).
668       */
669      public String getDbAlias() {
670        return dbAlias;
671      }
672
673      /**
674       * Return the bean property this column is mapped to.
675       */
676      public String getPropertyName() {
677        return propertyName;
678      }
679
680      /**
681       * Set the property name mapped to this db column.
682       */
683      private void setPropertyName(String propertyName) {
684        this.propertyName = propertyName;
685      }
686
687      /**
688       * Prepend the path to the property name.
689       * <p/>
690       * For example if path is "customer" then "name" becomes "customer.name".
691       */
692      public void tableAliasMapping(String path) {
693        if (path != null) {
694          propertyName = path + "." + propertyName;
695        }
696      }
697    }
698  }
699}