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   * Return the key;
211   */
212  public Key getKey() {
213    boolean parsed = sql != null && sql.parsed;
214    String unParsedSql = (sql == null) ? "" : sql.unparsedSql;
215    return new Key(parsed, unParsedSql, columnMapping);
216  }
217  
218  /**
219   * Return the resultSet if this is a ResultSet based RawSql.
220   */
221  public ResultSet getResultSet() {
222    return resultSet;
223  }
224
225  /**
226   * Return the column mapping for the SQL columns to bean properties.
227   */
228  public ColumnMapping getColumnMapping() {
229    return columnMapping;
230  }
231
232  /**
233   * Represents the sql part of the query. For parsed RawSql the sql is broken
234   * up so that Ebean can insert extra WHERE and HAVING expressions into the
235   * SQL.
236   */
237  public static final class Sql implements Serializable {
238
239    private static final long serialVersionUID = 1L;
240
241    private final boolean parsed;
242
243    private final String unparsedSql;
244
245    private final String preFrom;
246
247    private final String preWhere;
248
249    private final boolean andWhereExpr;
250
251    private final String preHaving;
252
253    private final boolean andHavingExpr;
254
255    private final String orderByPrefix;
256
257    private final String orderBy;
258
259    private final boolean distinct;
260
261    /**
262     * Construct for unparsed SQL.
263     */
264    protected Sql(String unparsedSql) {
265      this.parsed = false;
266      this.unparsedSql = unparsedSql;
267      this.preFrom = null;
268      this.preHaving = null;
269      this.preWhere = null;
270      this.andHavingExpr = false;
271      this.andWhereExpr = false;
272      this.orderByPrefix = null;
273      this.orderBy = null;
274      this.distinct = false;
275    }
276
277    /**
278     * Construct for parsed SQL.
279     */
280    protected Sql(String unparsedSql, String preFrom, String preWhere, boolean andWhereExpr,
281        String preHaving, boolean andHavingExpr, String orderByPrefix, String orderBy, boolean distinct) {
282
283      this.unparsedSql = unparsedSql;
284      this.parsed = true;
285      this.preFrom = preFrom;
286      this.preHaving = preHaving;
287      this.preWhere = preWhere;
288      this.andHavingExpr = andHavingExpr;
289      this.andWhereExpr = andWhereExpr;
290      this.orderByPrefix = orderByPrefix;
291      this.orderBy = orderBy;
292      this.distinct = distinct;
293    }
294
295    public String toString() {
296      if (!parsed) {
297        return "unparsed[" + unparsedSql + "]";
298      }
299      return "select[" + preFrom + "] preWhere[" + preWhere + "] preHaving[" + preHaving
300          + "] orderBy[" + orderBy + "]";
301    }
302
303    public boolean isDistinct() {
304      return distinct;
305    }
306
307    /**
308     * Return true if the SQL is left completely unmodified.
309     * <p>
310     * This means Ebean can't add WHERE or HAVING expressions into the query -
311     * it will be left completely unmodified.
312     * </p>
313     */
314    public boolean isParsed() {
315      return parsed;
316    }
317
318    /**
319     * Return the SQL when it is unparsed.
320     */
321    public String getUnparsedSql() {
322      return unparsedSql;
323    }
324
325    /**
326     * Return the SQL prior to FROM clause.
327     */
328    public String getPreFrom() {
329      return preFrom;
330    }
331
332    /**
333     * Return the SQL prior to WHERE clause.
334     */
335    public String getPreWhere() {
336      return preWhere;
337    }
338
339    /**
340     * Return true if there is already a WHERE clause and any extra where
341     * expressions start with AND.
342     */
343    public boolean isAndWhereExpr() {
344      return andWhereExpr;
345    }
346
347    /**
348     * Return the SQL prior to HAVING clause.
349     */
350    public String getPreHaving() {
351      return preHaving;
352    }
353
354    /**
355     * Return true if there is already a HAVING clause and any extra having
356     * expressions start with AND.
357     */
358    public boolean isAndHavingExpr() {
359      return andHavingExpr;
360    }
361
362    /**
363     * Return the 'order by' keywords.
364     * This can contain additional keywords, for example 'order siblings by' as Oracle syntax.
365     */
366    public String getOrderByPrefix() {
367      return (orderByPrefix == null) ? "order by" : orderByPrefix;
368    }
369
370    /**
371     * Return the SQL ORDER BY clause.
372     */
373    public String getOrderBy() {
374      return orderBy;
375    }
376
377  }
378
379  /**
380   * Defines the column mapping for raw sql DB columns to bean properties.
381   */
382  public static final class ColumnMapping implements Serializable {
383
384    private static final long serialVersionUID = 1L;
385
386    private final LinkedHashMap<String, Column> dbColumnMap;
387
388    private final Map<String, String> propertyMap;
389    
390    private final Map<String, Column> propertyColumnMap;
391
392    private final boolean parsed;
393
394    private final boolean immutable;
395
396    /**
397     * Construct from parsed sql where the columns have been identified.
398     */
399    protected ColumnMapping(List<Column> columns) {
400      this.immutable = false;
401      this.parsed = true;
402      this.propertyMap = null;
403      this.propertyColumnMap = null;
404      this.dbColumnMap = new LinkedHashMap<String, Column>();
405      for (int i = 0; i < columns.size(); i++) {
406        Column c = columns.get(i);
407        dbColumnMap.put(c.getDbColumnKey(), c);
408      }
409    }
410
411    /**
412     * Construct for unparsed sql.
413     */
414    protected ColumnMapping() {
415      this.immutable = false;
416      this.parsed = false;
417      this.propertyMap = null;
418      this.propertyColumnMap = null;
419      this.dbColumnMap = new LinkedHashMap<String, Column>();
420    }
421    
422    /**
423     * Construct for ResultSet use.
424     */
425    protected ColumnMapping(String... propertyNames) {
426      this.immutable = false;
427      this.parsed = false;
428      this.propertyMap = null;
429      this.dbColumnMap = new LinkedHashMap<String, Column>();
430
431      int pos = 0;
432      for (String prop : propertyNames) {
433        dbColumnMap.put(prop, new Column(pos++, prop, null, prop));
434      }
435      propertyColumnMap = dbColumnMap;
436    }
437
438    /**
439     * Construct an immutable ColumnMapping based on collected information.
440     */
441    protected ColumnMapping(boolean parsed, LinkedHashMap<String, Column> dbColumnMap) {
442      this.immutable = true;
443      this.parsed = parsed;
444      this.dbColumnMap = dbColumnMap;
445
446      HashMap<String, Column> pcMap = new HashMap<String, Column>();
447      HashMap<String, String> pMap = new HashMap<String, String>();
448
449      for (Column c : dbColumnMap.values()) {
450        pMap.put(c.getPropertyName(), c.getDbColumn());
451        pcMap.put(c.getPropertyName(), c);
452      }
453      this.propertyMap = Collections.unmodifiableMap(pMap);
454      this.propertyColumnMap = Collections.unmodifiableMap(pcMap);
455    }
456
457    @Override
458    public boolean equals(Object o) {
459      if (this == o) return true;
460      if (o == null || getClass() != o.getClass()) return false;
461      ColumnMapping that = (ColumnMapping) o;
462      return dbColumnMap.equals(that.dbColumnMap);
463    }
464
465    @Override
466    public int hashCode() {
467      return dbColumnMap.hashCode();
468    }
469
470    /**
471     * Return true if the property is mapped.
472     */
473    public boolean contains(String property) {
474      return this.propertyColumnMap.containsKey(property);
475    }
476
477    /**
478     * Creates an immutable copy of this ColumnMapping.
479     * 
480     * @throws IllegalStateException
481     *           when a propertyName has not been defined for a column.
482     */
483    protected ColumnMapping createImmutableCopy() {
484
485      for (Column c : dbColumnMap.values()) {
486        c.checkMapping();
487      }
488
489      return new ColumnMapping(parsed, dbColumnMap);
490    }
491
492    protected void columnMapping(String dbColumn, String propertyName) {
493
494      if (immutable) {
495        throw new IllegalStateException("Should never happen");
496      }
497      if (!parsed) {
498        int pos = dbColumnMap.size();
499        dbColumnMap.put(dbColumn, new Column(pos, dbColumn, null, propertyName));
500      } else {
501        Column column = dbColumnMap.get(dbColumn);
502        if (column == null) {
503          String msg = "DB Column [" + dbColumn + "] not found in mapping. Expecting one of [" + dbColumnMap.keySet() + "]";
504          throw new IllegalArgumentException(msg);
505        }
506        column.setPropertyName(propertyName);
507      }
508    }
509
510    /**
511     * Returns true if the Columns where supplied by parsing the sql select
512     * clause.
513     * <p>
514     * In the case where the columns where parsed then we can do extra checks on
515     * the column mapping such as, is the column a valid one in the sql and
516     * whether all the columns in the sql have been mapped.
517     * </p>
518     */
519    public boolean isParsed() {
520      return parsed;
521    }
522
523    /**
524     * Return the number of columns in this column mapping.
525     */
526    public int size() {
527      return dbColumnMap.size();
528    }
529
530    /**
531     * Return the column mapping.
532     */
533    protected Map<String, Column> mapping() {
534      return dbColumnMap;
535    }
536
537    /**
538     * Return the mapping by DB column.
539     */
540    public Map<String, String> getMapping() {
541      return propertyMap;
542    }
543
544    /**
545     * Return the index position by bean property name.
546     */
547    public int getIndexPosition(String property) {
548      Column c = propertyColumnMap.get(property);
549      return c == null ? -1 : c.getIndexPos();
550    }
551
552    /**
553     * Return an iterator of the Columns.
554     */
555    public Iterator<Column> getColumns() {
556      return dbColumnMap.values().iterator();
557    }
558
559    /**
560     * Modify any column mappings with the given table alias to have the path prefix.
561     * <p>
562     * For example modify all mappings with table alias "c" to have the path prefix "customer".
563     * </p>
564     * <p>
565     * For the "Root type" you don't need to specify a tableAliasMapping.
566     * </p>
567     */
568    public void tableAliasMapping(String tableAlias, String path) {
569
570      String startMatch = tableAlias+".";
571      for (Map.Entry<String, Column> entry : dbColumnMap.entrySet()) {
572        if (entry.getKey().startsWith(startMatch)) {
573          entry.getValue().tableAliasMapping(path);
574        }
575      }
576    }
577
578    /**
579     * A Column of the RawSql that is mapped to a bean property (or ignored).
580     */
581    public static class Column implements Serializable {
582
583      private static final long serialVersionUID = 1L;
584      private final int indexPos;
585      private final String dbColumn;
586
587      private final String dbAlias;
588
589      private String propertyName;
590
591      /**
592       * Construct a Column.
593       */
594      public Column(int indexPos, String dbColumn, String dbAlias) {
595        this(indexPos, dbColumn, dbAlias, derivePropertyName(dbAlias, dbColumn));
596      }
597
598      private Column(int indexPos, String dbColumn, String dbAlias, String propertyName) {
599        this.indexPos = indexPos;
600        this.dbColumn = dbColumn;
601        this.dbAlias = dbAlias;
602        if (propertyName == null && dbAlias != null) {
603          this.propertyName = dbAlias;
604        } else {
605          this.propertyName = propertyName;
606        }
607      }
608
609      protected static String derivePropertyName(String dbAlias, String dbColumn) {
610        if (dbAlias != null) {
611          return CamelCaseHelper.toCamelFromUnderscore(dbAlias);
612        }
613        int dotPos = dbColumn.indexOf('.');
614        if (dotPos > -1) {
615          dbColumn = dbColumn.substring(dotPos + 1);
616        }
617        return CamelCaseHelper.toCamelFromUnderscore(dbColumn);
618      }
619
620      private void checkMapping() {
621        if (propertyName == null) {
622          String msg = "No propertyName defined (Column mapping) for dbColumn [" + dbColumn + "]";
623          throw new IllegalStateException(msg);
624        }
625      }
626
627      @Override
628      public boolean equals(Object o) {
629        if (this == o) return true;
630        if (o == null || getClass() != o.getClass()) return false;
631
632        Column that = (Column) o;
633        if (indexPos != that.indexPos) return false;
634        if (!dbColumn.equals(that.dbColumn)) return false;
635        if (dbAlias != null ? !dbAlias.equals(that.dbAlias) : that.dbAlias != null) return false;
636        return propertyName != null ? propertyName.equals(that.propertyName) : that.propertyName == null;
637      }
638
639      @Override
640      public int hashCode() {
641        int result = indexPos;
642        result = 31 * result + dbColumn.hashCode();
643        result = 31 * result + (dbAlias != null ? dbAlias.hashCode() : 0);
644        result = 31 * result + (propertyName != null ? propertyName.hashCode() : 0);
645        return result;
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 alias if specified otherwise DB column.
661       * This is used as the key for mapping a column to a logical property.
662       */
663      public String getDbColumnKey() {
664        return (dbAlias != null) ? dbAlias : dbColumn;
665      }
666
667      /**
668       * Return the DB column name including table alias (if it has one).
669       */
670      public String getDbColumn() {
671        return dbColumn;
672      }
673
674      /**
675       * Return the bean property this column is mapped to.
676       */
677      public String getPropertyName() {
678        return propertyName;
679      }
680
681      /**
682       * Set the property name mapped to this db column.
683       */
684      private void setPropertyName(String propertyName) {
685        this.propertyName = propertyName;
686      }
687
688      /**
689       * Prepend the path to the property name.
690       * <p/>
691       * For example if path is "customer" then "name" becomes "customer.name".
692       */
693      public void tableAliasMapping(String path) {
694        if (path != null) {
695          propertyName = path + "." + propertyName;
696        }
697      }
698    }
699  }
700
701  /**
702   * A key for the RawSql object using for the query plan.
703   */
704  public static final class Key {
705
706    private final boolean parsed;
707    private final ColumnMapping columnMapping;
708    private final String unParsedSql;
709
710    Key(boolean parsed, String unParsedSql, ColumnMapping columnMapping) {
711      this.parsed = parsed;
712      this.unParsedSql = unParsedSql;
713      this.columnMapping = columnMapping;
714    }
715
716    @Override
717    public boolean equals(Object o) {
718      if (this == o) return true;
719      if (o == null || getClass() != o.getClass()) return false;
720
721      Key that = (Key) o;
722      return parsed == that.parsed
723          && columnMapping.equals(that.columnMapping)
724          && unParsedSql.equals(that.unParsedSql);
725    }
726
727    @Override
728    public int hashCode() {
729      int result = (parsed ? 1 : 0);
730      result = 31 * result + columnMapping.hashCode();
731      result = 31 * result + unParsedSql.hashCode();
732      return result;
733    }
734  }
735}