View Javadoc

1   package org.apache.torque.util;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import org.apache.commons.lang.StringUtils;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.torque.Torque;
32  import org.apache.torque.TorqueException;
33  import org.apache.torque.adapter.DB;
34  import org.apache.torque.map.ColumnMap;
35  import org.apache.torque.map.DatabaseMap;
36  import org.apache.torque.util.Criteria.Criterion;
37  
38  /***
39   * Factored out code that is used to process SQL tables. This code comes
40   * from BasePeer and is put here to reduce complexity in the BasePeer class.
41   * You should not use the methods here directly!
42   *
43   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
44   * @author <a href="mailto:fischer@seitenbau.de">Thomas Fischer</a>
45   * @version $Id: SQLBuilder.java 535596 2007-05-06 10:47:39Z tfischer $
46   */
47  public final class SQLBuilder
48  {
49      /*** Logging */
50      protected static final Log log = LogFactory.getLog(SQLBuilder.class);
51  
52      /*** Function Characters */
53      public static final String[] COLUMN_CHARS = {".", "*"};
54      public static final String[] DELIMITERS = {" ", ",", "(", ")", "<", ">"};
55  
56      /***
57       * Private constructor to prevent instantiation.
58       *
59       * Class contains only static method ans should therefore not be
60       * instantiated.
61       */
62      private SQLBuilder()
63      {
64      }
65  
66      /***
67       * Fully qualify a table name with an optional schema reference
68       *
69       * @param table The table name to use. If null is passed in, null is returned.
70       * @param dbName The name of the database to which this tables belongs.
71       *               If null is passed, the default database is used.
72       *
73       * @return The table name to use inside the SQL statement. If null is passed
74       *         into this method, null is returned.
75       * @exception TorqueException if an error occurs
76       */
77      public static String getFullTableName(
78              final String table,
79              final String dbName)
80          throws TorqueException
81      {
82          if (table != null)
83          {
84              int dotIndex = table.indexOf(".");
85  
86              if (dotIndex == -1) // No schema given
87              {
88                  String targetDBName = (dbName == null)
89                          ? Torque.getDefaultDB()
90                          : dbName;
91  
92                  String targetSchema = Torque.getSchema(targetDBName);
93  
94                  // If we have a default schema, fully qualify the
95                  // table and return.
96                  if (StringUtils.isNotEmpty(targetSchema))
97                  {
98                      return new StringBuffer()
99                              .append(targetSchema)
100                             .append(".")
101                             .append(table)
102                             .toString();
103                 }
104             }
105         }
106 
107         return table;
108     }
109 
110     /***
111      * Remove a possible schema name from the table name.
112      *
113      * @param table The table name to use
114      *
115      * @return The table name with a possible schema name
116      *         stripped off
117      */
118     public static String getUnqualifiedTableName(final String table)
119     {
120         if (table != null)
121         {
122             int dotIndex = table.lastIndexOf("."); // Do we have a dot?
123 
124             if (++dotIndex > 0) // Incrementation allows for better test _and_ substring...
125             {
126                 return table.substring(dotIndex);
127             }
128         }
129 
130         return table;
131     }
132 
133     /***
134      * Removes a possible function name or clause from a column name
135      *
136      * @param name The column name, possibly containing a clause
137      *
138      * @return The column name
139      *
140      * @throws TorqueException If the column name was malformed
141      */
142     private static String removeSQLFunction(final String name)
143             throws TorqueException
144     {
145         // Empty name => return it
146         if (StringUtils.isEmpty(name))
147         {
148             return name;
149         }
150 
151         // Find Table.Column
152         int dotIndex = name.indexOf('.');
153         if (dotIndex == -1)
154         {
155             dotIndex = name.indexOf("*");
156         }
157         if (dotIndex == -1)
158         {
159             throw new TorqueException("removeSQLFunction() : Column name "
160                     + name
161                     + " does not contain a . or a *");
162         }
163         String pre = name.substring(0, dotIndex);
164         String post = name.substring(dotIndex + 1, name.length());
165         int startIndex = StringUtils.lastIndexOfAny(pre, DELIMITERS);
166         int endIndex = StringUtils.indexOfAny(post, DELIMITERS);
167         if (startIndex < 0 && endIndex < 0)
168         {
169             return name;
170         }
171         else
172         {
173             if (endIndex < 0)
174             {
175                 endIndex = post.length();
176             }
177             // if startIndex == -1 the formula is correct
178             return name.substring(startIndex + 1, dotIndex + 1 + endIndex);
179         }
180     }
181 
182     /***
183      * Returns a table name from an identifier. Each identifier is to be qualified
184      * as [schema.]table.column. This could also contain FUNCTION([schema.]table.column).
185      *
186      * @param name The (possible fully qualified) identifier name
187      *
188      * @return the fully qualified table name
189      *
190      * @throws TorqueException If the identifier name was malformed
191      */
192     public static String getTableName(final String name, final String dbName)
193             throws TorqueException
194     {
195         final String testName = removeSQLFunction(name);
196 
197         if (StringUtils.isEmpty(testName))
198         {
199             throwMalformedColumnNameException(
200                     "getTableName",
201                     name);
202         }
203 
204         // Everything before the last dot is the table name
205         int rightDotIndex = testName.lastIndexOf('.');
206 
207         if (rightDotIndex < 0)
208         {
209             if ("*".equals(testName))
210             {
211                 return null;
212             }
213 
214             throwMalformedColumnNameException(
215                     "getTableName",
216                     name);
217         }
218 
219         return getFullTableName(testName.substring(0, rightDotIndex), dbName);
220     }
221 
222 
223 
224     /***
225      * Returns a set of all tables and possible aliases referenced
226      * from a criterion. The resulting Set can be directly used to
227      * build a WHERE clause
228      *
229      * @param crit A Criteria object
230      * @param tableCallback A Callback Object
231      * @return A Set of tables.
232      */
233     public static Set getTableSet(
234             final Criteria crit,
235             final TableCallback tableCallback)
236     {
237         HashSet tables = new HashSet();
238 
239         // Loop over all the Criterions
240         for (Iterator it = crit.keySet().iterator(); it.hasNext();)
241         {
242             String key = (String) it.next();
243             Criteria.Criterion c = crit.getCriterion(key);
244             List tableNames = c.getAllTables();
245 
246             // Loop over all Tables referenced in this criterion.
247             for (Iterator it2 = tableNames.iterator(); it2.hasNext();)
248             {
249                 String name = (String) it2.next();
250                 String aliasName = crit.getTableForAlias(name);
251 
252                 // If the tables have an alias, add an "<xxx> AS <yyy> statement"
253                 if (StringUtils.isNotEmpty(aliasName))
254                 {
255                     String newName =
256                             new StringBuffer(name.length() + aliasName.length() + 4)
257                             .append(aliasName)
258                             .append(" AS ")
259                             .append(name)
260                             .toString();
261                     name = newName;
262                 }
263                 tables.add(name);
264             }
265 
266             if (tableCallback != null)
267             {
268                 tableCallback.process(tables, key, crit);
269             }
270         }
271 
272         return tables;
273     }
274 
275     /***
276      * Builds a Query clause for Updating and deleting
277      *
278      * @param crit a <code>Criteria</code> value
279      * @param params a <code>List</code> value
280      * @param qc a <code>QueryCallback</code> value
281      * @return a <code>Query</code> value
282      * @exception TorqueException if an error occurs
283      */
284     public static Query buildQueryClause(final Criteria crit,
285             final List params,
286             final QueryCallback qc)
287             throws TorqueException
288     {
289         Query query = new Query();
290 
291         final String dbName = crit.getDbName();
292         final DB db = Torque.getDB(dbName);
293         final DatabaseMap dbMap = Torque.getDatabaseMap(dbName);
294 
295         JoinBuilder.processJoins(db, dbMap, crit, query);
296         processModifiers(crit, query);
297         processSelectColumns(crit, query, dbName);
298         processAsColumns(crit, query);
299         processCriterions(db, dbMap, dbName, crit, query,  params, qc);
300         processGroupBy(crit, query);
301         processHaving(crit, query);
302         processOrderBy(db, dbMap, crit, query);
303         processLimits(crit, query);
304 
305         if (log.isDebugEnabled())
306         {
307             log.debug(query.toString());
308         }
309         return query;
310     }
311 
312 
313     /***
314      * adds the select columns from the criteria to the query
315      * @param criteria the criteria from which the select columns are taken
316      * @param query the query to which the select columns should be added
317      * @throws TorqueException if the select columns can not be processed
318      */
319     private static void processSelectColumns(
320             final Criteria criteria,
321             final Query query,
322             final String dbName)
323         throws TorqueException
324     {
325         UniqueList selectClause = query.getSelectClause();
326         UniqueList select = criteria.getSelectColumns();
327 
328         for (int i = 0; i < select.size(); i++)
329         {
330             String identifier = (String) select.get(i);
331             selectClause.add(identifier);
332             addTableToFromClause(getTableName(identifier, dbName), criteria, query);
333         }
334     }
335 
336     /***
337      * adds the As-columns from the criteria to the query.
338      * @param criteria the criteria from which the As-columns are taken
339      * @param query the query to which the As-columns should be added
340      */
341     private static void processAsColumns(
342             final Criteria criteria,
343             final Query query)
344     {
345         UniqueList querySelectClause = query.getSelectClause();
346         Map criteriaAsColumns = criteria.getAsColumns();
347 
348         for (Iterator it = criteriaAsColumns.entrySet().iterator(); it.hasNext();)
349         {
350             Map.Entry entry = (Map.Entry) it.next();
351             String key = (String) entry.getKey();
352             querySelectClause.add(
353                     new StringBuffer()
354                     .append(entry.getValue())
355                     .append(SqlEnum.AS)
356                     .append(key)
357                     .toString());
358         }
359     }
360 
361     /***
362      * adds the Modifiers from the criteria to the query
363      * @param criteria the criteria from which the Modifiers are taken
364      * @param query the query to which the Modifiers should be added
365      */
366     private static void processModifiers(
367             final Criteria criteria,
368             final Query query)
369     {
370         UniqueList selectModifiers = query.getSelectModifiers();
371         UniqueList modifiers = criteria.getSelectModifiers();
372         for (int i = 0; i < modifiers.size(); i++)
373         {
374             selectModifiers.add(modifiers.get(i));
375         }
376     }
377 
378     /***
379      * adds the Criterion-objects from the criteria to the query
380      * @param criteria the criteria from which the Criterion-objects are taken
381      * @param query the query to which the Criterion-objects should be added
382      * @param params the parameters if a prepared statement should be built,
383      *        or null if a normal statement should be built.
384      * @throws TorqueException if the Criterion-objects can not be processed
385      */
386     private static void processCriterions(
387             final DB db,
388             final DatabaseMap dbMap,
389             final String dbName,
390             final Criteria crit,
391             final Query query,
392             final List params,
393             final QueryCallback qc)
394         throws TorqueException
395     {
396         UniqueList whereClause = query.getWhereClause();
397 
398         for (Iterator it = crit.keySet().iterator(); it.hasNext();)
399         {
400             String key = (String) it.next();
401             Criteria.Criterion criterion = crit.getCriterion(key);
402             Criteria.Criterion[] someCriteria =
403                     criterion.getAttachedCriterion();
404 
405             String table = null;
406             for (int i = 0; i < someCriteria.length; i++)
407             {
408                 String tableName = someCriteria[i].getTable();
409 
410                 // add the table to the from clause, if it is not already
411                 // contained there
412                 // it is important that this piece of code is executed AFTER
413                 // the joins are processed
414                 addTableToFromClause(getFullTableName(tableName, dbName), crit, query);
415 
416                 table = crit.getTableForAlias(tableName);
417                 if (table == null)
418                 {
419                     table = tableName;
420                 }
421 
422                 boolean ignoreCase =  ((crit.isIgnoreCase() || someCriteria[i].isIgnoreCase())
423                         && (dbMap.getTable(table)
424                                 .getColumn(someCriteria[i].getColumn())
425                                 .getType()
426                                 instanceof String));
427 
428                 someCriteria[i].setIgnoreCase(ignoreCase);
429             }
430 
431             criterion.setDB(db);
432             whereClause.add(qc.process(criterion, params));
433         }
434     }
435 
436     /***
437      * adds the OrderBy-Columns from the criteria to the query
438      * @param criteria the criteria from which the OrderBy-Columns are taken
439      * @param query the query to which the OrderBy-Columns should be added
440      * @throws TorqueException if the OrderBy-Columns can not be processed
441      */
442     private static void processOrderBy(
443             final DB db,
444             final DatabaseMap dbMap,
445             final Criteria crit,
446             final Query query)
447             throws TorqueException
448     {
449         UniqueList orderByClause = query.getOrderByClause();
450         UniqueList selectClause = query.getSelectClause();
451 
452         UniqueList orderBy = crit.getOrderByColumns();
453 
454         if (orderBy != null && orderBy.size() > 0)
455         {
456             // Check for each String/Character column and apply
457             // toUpperCase().
458             for (int i = 0; i < orderBy.size(); i++)
459             {
460                 String orderByColumn = (String) orderBy.get(i);
461 
462                 String strippedColumnName
463                         = removeSQLFunction(orderByColumn);
464                 int dotPos = strippedColumnName.lastIndexOf('.');
465                 if (dotPos == -1)
466                 {
467                     // We are not able to look up the table in the
468                     // tableMap, as no table name is given. Simply add
469                     // the orderBy and hope the user knows what he is
470                     // doing.
471                     orderByClause.add(orderByColumn);
472                     continue;
473                 }
474 
475                 String tableName = strippedColumnName.substring(0, dotPos);
476                 String table = crit.getTableForAlias(tableName);
477                 if (table == null)
478                 {
479                     table = tableName;
480                 }
481 
482                 // See if there's a space (between the column list and sort
483                 // order in ORDER BY table.column DESC).
484                 int spacePos = strippedColumnName.indexOf(' ');
485                 String columnName;
486                 if (spacePos == -1)
487                 {
488                     columnName =
489                             strippedColumnName.substring(dotPos + 1);
490                 }
491                 else
492                 {
493                     columnName = strippedColumnName.substring(
494                             dotPos + 1,
495                             spacePos);
496                 }
497                 ColumnMap column = dbMap.getTable(table).getColumn(columnName);
498 
499                 // only ignore case in order by for string columns
500                 // which do not have a function around them
501                 if (column.getType() instanceof String
502                         && orderByColumn.indexOf('(') == -1)
503                 {
504                     // find space pos relative to orderByColumn
505                     spacePos = orderByColumn.indexOf(' ');
506                     if (spacePos == -1)
507                     {
508                         orderByClause.add(
509                                 db.ignoreCaseInOrderBy(orderByColumn));
510                     }
511                     else
512                     {
513                         orderByClause.add(
514                                 db.ignoreCaseInOrderBy(
515                                         orderByColumn.substring(0, spacePos))
516                                 + orderByColumn.substring(spacePos));
517                     }
518                     selectClause.add(
519                             db.ignoreCaseInOrderBy(tableName + '.' + columnName));
520                 }
521                 else
522                 {
523                     orderByClause.add(orderByColumn);
524                 }
525             }
526         }
527     }
528 
529     /***
530      * adds the GroupBy-Columns from the criteria to the query
531      * @param criteria the criteria from which the GroupBy-Columns are taken
532      * @param query the query to which the GroupBy-Columns should be added
533      * @throws TorqueException if the GroupBy-Columns can not be processed
534      */
535     private static void processGroupBy(
536             final Criteria crit,
537             final Query query)
538             throws TorqueException
539     {
540         UniqueList groupByClause = query.getGroupByClause();
541         UniqueList groupBy = crit.getGroupByColumns();
542 
543         // need to allow for multiple group bys
544         if (groupBy != null)
545         {
546             for (int i = 0; i < groupBy.size(); i++)
547             {
548                 String columnName = (String) groupBy.get(i);
549                 String column = (String) crit.getAsColumns().get(columnName);
550 
551                 if (column == null)
552                 {
553                     column = columnName;
554                 }
555 
556                 if (column.indexOf('.') != -1)
557                 {
558                     groupByClause.add(column);
559                 }
560                 else
561                 {
562                     throwMalformedColumnNameException("group by",
563                             column);
564                 }
565             }
566         }
567     }
568 
569     /***
570      * adds the Having-Columns from the criteria to the query
571      * @param criteria the criteria from which the Having-Columns are taken
572      * @param query the query to which the Having-Columns should be added
573      * @throws TorqueException if the Having-Columns can not be processed
574      */
575     private static void processHaving(
576             final Criteria crit,
577             final Query query)
578             throws TorqueException
579     {
580         Criteria.Criterion having = crit.getHaving();
581         if (having != null)
582         {
583             //String groupByString = null;
584             query.setHaving(having.toString());
585         }
586     }
587 
588     /***
589      * adds a Limit clause to the query if supported by the database
590      * @param criteria the criteria from which the Limit and Offset values
591      *        are taken
592      * @param query the query to which the Limit clause should be added
593      * @throws TorqueException if the Database adapter cannot be obtained
594      */
595     private static void processLimits(
596             final Criteria crit,
597             final Query query)
598             throws TorqueException
599     {
600         int limit = crit.getLimit();
601         int offset = crit.getOffset();
602 
603         if (offset > 0 || limit >= 0)
604         {
605             DB db = Torque.getDB(crit.getDbName());
606             db.generateLimits(query, offset, limit);
607         }
608     }
609 
610     /***
611      * Throws a TorqueException with the malformed column name error
612      * message.  The error message looks like this:<p>
613      *
614      * <code>
615      *     Malformed column name in Criteria [criteriaPhrase]:
616      *     '[columnName]' is not of the form 'table.column'
617      * </code>
618      *
619      * @param criteriaPhrase a String, one of "select", "join", or "order by"
620      * @param columnName a String containing the offending column name
621      * @throws TorqueException Any exceptions caught during processing will be
622      *         rethrown wrapped into a TorqueException.
623      */
624     public static void throwMalformedColumnNameException(
625         final String criteriaPhrase,
626         final String columnName)
627         throws TorqueException
628     {
629         StringBuffer sb = new StringBuffer()
630                 .append("Malformed column name in Criteria ")
631                 .append(criteriaPhrase)
632                 .append(": '")
633                 .append(StringUtils.isEmpty(columnName) ? "<empty>" : columnName)
634                 .append("' is not of the form 'table.column'");
635 
636         throw new TorqueException(sb.toString());
637     }
638 
639     /***
640      * Returns the tablename which can be added to a From Clause.
641      * This takes care of any aliases that might be defined.
642      * For example, if an alias "a" for the table AUTHOR is defined
643      * in the Criteria criteria, getTableNameForFromClause("a", criteria)
644      * returns "AUTHOR a".
645      * @param tableName the name of a table
646      *        or the alias for a table
647      * @param criteria a criteria object to resolve a possible alias
648      * @return either the tablename itself if tableOrAliasName is not an alias,
649      *         or a String of the form "tableName tableOrAliasName"
650      *         if tableOrAliasName is an alias for a table name
651      */
652     public static String getTableNameForFromClause(
653             final String tableName,
654             final Criteria criteria)
655     {
656         String shortTableName = getUnqualifiedTableName(tableName);
657 
658         // Most of the time, the alias would be for the short name...
659         String aliasName = criteria.getTableForAlias(shortTableName);
660         if (StringUtils.isEmpty(aliasName))
661         {
662             // But we should also check the FQN...
663             aliasName = criteria.getTableForAlias(tableName);
664         }
665 
666         if (StringUtils.isNotEmpty(aliasName))
667         {
668             // If the tables have an alias, add an "<xxx> <yyy> statement"
669             // <xxx> AS <yyy> causes problems on oracle
670             return new StringBuffer(
671                     tableName.length() + aliasName.length() + 1)
672                     .append(aliasName)
673                     .append(" ")
674                     .append(tableName)
675                     .toString();
676         }
677 
678         return tableName;
679     }
680 
681     /***
682      * Checks if the Tablename tableName is already contained in a from clause.
683      * If tableName and the tablenames in fromClause are generated by
684      * getTablenameForFromClause(String, Criteria), (which they usually are),
685      * then different aliases for the same table are treated
686      * as different tables: E.g.
687      * fromClauseContainsTableName(fromClause, "table_a a") returns false if
688      * fromClause contains only another alias for table_a ,
689      * e.g. "table_a aa" and the unaliased tablename "table_a".
690      * Special case: If tableName is null, true is returned.
691      * @param fromClause a list containing only elements of type.
692      *        Query.FromElement
693      * @param tableName the tablename to check
694      * @return if the Tablename tableName is already contained in a from clause.
695      *         If tableName is null, true is returned.
696      */
697     public static boolean fromClauseContainsTableName(
698             final UniqueList fromClause,
699             final String tableName)
700     {
701         if (tableName == null)
702         {
703             // usually this function is called to see if tableName should be
704             // added to the fromClause. As null should not be added,
705             // true is returned.
706             return true;
707         }
708         for (Iterator it = fromClause.iterator(); it.hasNext();)
709         {
710             Query.FromElement fromElement
711                     = (Query.FromElement) it.next();
712             if (tableName.equals(fromElement.getTableName()))
713             {
714                 return true;
715             }
716         }
717         return false;
718     }
719 
720     /***
721      * adds a table to the from clause of a query, if it is not already
722      * contained there.
723      * @param tableOrAliasName the name of a table
724      *        or the alias for a table
725      * @param criteria a criteria object to resolve a possible alias
726      * @param query the query where the the tablename should be added
727      *        to the from clause
728      * @return the table in the from clause which represents the
729      *         supplied tableOrAliasName
730      */
731     private static String addTableToFromClause(
732             final String tableName,
733             final Criteria criteria,
734             Query query)
735     {
736         String tableNameForFromClause
737                 = getTableNameForFromClause(tableName, criteria);
738 
739         UniqueList queryFromClause = query.getFromClause();
740 
741         // it is important that this piece of code is executed AFTER
742         // the joins are processed
743         if (!fromClauseContainsTableName(
744             queryFromClause,
745             tableNameForFromClause))
746         {
747             Query.FromElement fromElement
748                     = new Query.FromElement(
749                             tableNameForFromClause, null, null);
750             queryFromClause.add(fromElement);
751         }
752         return tableNameForFromClause;
753     }
754 
755     /***
756      * Inner Interface that defines the Callback method for
757      * the Table creation loop.
758      */
759     public interface TableCallback
760     {
761         /***
762          * Callback Method for getTableSet()
763          *
764          * @param tables The current table name
765          * @param key The current criterion key.
766          * @param crit The Criteria used in getTableSet()
767          */
768         void process(Set tables, String key, Criteria crit);
769     }
770 
771     /***
772      * Inner Interface that defines the Callback method for
773      * the buildQuery Criterion evaluation
774      */
775     public interface QueryCallback
776     {
777         /***
778          * The callback for building a query String
779          *
780          * @param criterion The current criterion
781          * @param params The parameter list passed to buildQueryString()
782          * @return WHERE SQL fragment for this criterion
783          */
784         String process(Criterion criterion, List params);
785     }
786 
787 }