1 package org.apache.torque.util;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
87 {
88 String targetDBName = (dbName == null)
89 ? Torque.getDefaultDB()
90 : dbName;
91
92 String targetSchema = Torque.getSchema(targetDBName);
93
94
95
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(".");
123
124 if (++dotIndex > 0)
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
146 if (StringUtils.isEmpty(name))
147 {
148 return name;
149 }
150
151
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
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
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
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
247 for (Iterator it2 = tableNames.iterator(); it2.hasNext();)
248 {
249 String name = (String) it2.next();
250 String aliasName = crit.getTableForAlias(name);
251
252
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
411
412
413
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
457
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
468
469
470
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
483
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
500
501 if (column.getType() instanceof String
502 && orderByColumn.indexOf('(') == -1)
503 {
504
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
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
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
659 String aliasName = criteria.getTableForAlias(shortTableName);
660 if (StringUtils.isEmpty(aliasName))
661 {
662
663 aliasName = criteria.getTableForAlias(tableName);
664 }
665
666 if (StringUtils.isNotEmpty(aliasName))
667 {
668
669
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
704
705
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
742
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 }