/* * Hibernate, Relational Persistence for Idiomatic Java * * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat Middleware LLC. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA * */ package org.hibernate.sql; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import org.hibernate.HibernateException; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.function.SQLFunction; import org.hibernate.dialect.function.SQLFunctionRegistry; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.StringHelper; import org.hibernate.sql.ordering.antlr.ColumnMapper; import org.hibernate.sql.ordering.antlr.OrderByFragmentTranslator; import org.hibernate.sql.ordering.antlr.TranslationContext; /** * Parses SQL fragments specified in mapping documents * * @author Gavin King */ public final class Template { private static final Set<String> KEYWORDS = new HashSet<String>(); private static final Set<String> BEFORE_TABLE_KEYWORDS = new HashSet<String>(); private static final Set<String> FUNCTION_KEYWORDS = new HashSet<String>(); static { KEYWORDS.add("and"); KEYWORDS.add("or"); KEYWORDS.add("not"); KEYWORDS.add("like"); KEYWORDS.add("is"); KEYWORDS.add("in"); KEYWORDS.add("between"); KEYWORDS.add("null"); KEYWORDS.add("select"); KEYWORDS.add("distinct"); KEYWORDS.add("from"); KEYWORDS.add("join"); KEYWORDS.add("inner"); KEYWORDS.add("outer"); KEYWORDS.add("left"); KEYWORDS.add("right"); KEYWORDS.add("on"); KEYWORDS.add("where"); KEYWORDS.add("having"); KEYWORDS.add("group"); KEYWORDS.add("order"); KEYWORDS.add("by"); KEYWORDS.add("desc"); KEYWORDS.add("asc"); KEYWORDS.add("limit"); KEYWORDS.add("any"); KEYWORDS.add("some"); KEYWORDS.add("exists"); KEYWORDS.add("all"); KEYWORDS.add("union"); KEYWORDS.add("minus"); BEFORE_TABLE_KEYWORDS.add("from"); BEFORE_TABLE_KEYWORDS.add("join"); FUNCTION_KEYWORDS.add("as"); FUNCTION_KEYWORDS.add("leading"); FUNCTION_KEYWORDS.add("trailing"); FUNCTION_KEYWORDS.add("from"); FUNCTION_KEYWORDS.add("case"); FUNCTION_KEYWORDS.add("when"); FUNCTION_KEYWORDS.add("then"); FUNCTION_KEYWORDS.add("else"); FUNCTION_KEYWORDS.add("end"); } public static final String TEMPLATE = "$PlaceHolder$"; private Template() {} public static String renderWhereStringTemplate(String sqlWhereString, Dialect dialect, SQLFunctionRegistry functionRegistry) { return renderWhereStringTemplate(sqlWhereString, TEMPLATE, dialect, functionRegistry); } /** * Same functionality as {@link #renderWhereStringTemplate(String, String, Dialect, SQLFunctionRegistry)}, * except that a SQLFunctionRegistry is not provided (i.e., only the dialect-defined functions are * considered). This is only intended for use by the annotations project until the * many-to-many/map-key-from-target-table feature is pulled into core. * * @deprecated Only intended for annotations usage; use {@link #renderWhereStringTemplate(String, String, Dialect, SQLFunctionRegistry)} instead */ @Deprecated @SuppressWarnings({ "JavaDoc" }) public static String renderWhereStringTemplate(String sqlWhereString, String placeholder, Dialect dialect) { return renderWhereStringTemplate( sqlWhereString, placeholder, dialect, new SQLFunctionRegistry( dialect, java.util.Collections.EMPTY_MAP ) ); } /** * Takes the where condition provided in the mapping attribute and interpolates the alias. * Handles sub-selects, quoted identifiers, quoted strings, expressions, SQL functions, * named parameters. * * @param sqlWhereString The string into which to interpolate the placeholder value * @param placeholder The value to be interpolated into the the sqlWhereString * @param dialect The dialect to apply * @param functionRegistry The registry of all sql functions * @return The rendered sql fragment */ public static String renderWhereStringTemplate(String sqlWhereString, String placeholder, Dialect dialect, SQLFunctionRegistry functionRegistry ) { // IMPL NOTE : The basic process here is to tokenize the incoming string and to iterate over each token // in turn. As we process each token, we set a series of flags used to indicate the type of context in // which the tokens occur. Depending on the state of those flags we decide whether we need to qualify // identifier references. String symbols = new StringBuffer() .append( "=><!+-*/()',|&`" ) .append( StringHelper.WHITESPACE ) .append( dialect.openQuote() ) .append( dialect.closeQuote() ) .toString(); StringTokenizer tokens = new StringTokenizer( sqlWhereString, symbols, true ); StringBuffer result = new StringBuffer(); boolean quoted = false; boolean quotedIdentifier = false; boolean beforeTable = false; boolean inFromClause = false; boolean afterFromTable = false; boolean hasMore = tokens.hasMoreTokens(); String nextToken = hasMore ? tokens.nextToken() : null; while ( hasMore ) { String token = nextToken; String lcToken = token.toLowerCase(); hasMore = tokens.hasMoreTokens(); nextToken = hasMore ? tokens.nextToken() : null; boolean isQuoteCharacter = false; if ( !quotedIdentifier && "'".equals(token) ) { quoted = !quoted; isQuoteCharacter = true; } if ( !quoted ) { boolean isOpenQuote; if ( "`".equals(token) ) { isOpenQuote = !quotedIdentifier; token = lcToken = isOpenQuote ? Character.toString( dialect.openQuote() ) : Character.toString( dialect.closeQuote() ); quotedIdentifier = isOpenQuote; isQuoteCharacter = true; } else if ( !quotedIdentifier && ( dialect.openQuote()==token.charAt(0) ) ) { isOpenQuote = true; quotedIdentifier = true; isQuoteCharacter = true; } else if ( quotedIdentifier && ( dialect.closeQuote()==token.charAt(0) ) ) { quotedIdentifier = false; isQuoteCharacter = true; isOpenQuote = false; } else { isOpenQuote = false; } if ( isOpenQuote ) { result.append( placeholder ).append( '.' ); } } // Special processing for ANSI SQL EXTRACT function if ( "extract".equals( lcToken ) && "(".equals( nextToken ) ) { final String field = extractUntil( tokens, "from" ); final String source = renderWhereStringTemplate( extractUntil( tokens, ")" ), placeholder, dialect, functionRegistry ); result.append( "extract(" ).append( field ).append( " from " ).append( source ).append( ')' ); hasMore = tokens.hasMoreTokens(); nextToken = hasMore ? tokens.nextToken() : null; continue; } // Special processing for ANSI SQL TRIM function if ( "trim".equals( lcToken ) && "(".equals( nextToken ) ) { List<String> operands = new ArrayList<String>(); StringBuilder builder = new StringBuilder(); boolean hasMoreOperands = true; String operandToken = tokens.nextToken(); boolean quotedOperand = false; while ( hasMoreOperands ) { final boolean isQuote = "'".equals( operandToken ); if ( isQuote ) { quotedOperand = !quotedOperand; if ( !quotedOperand ) { operands.add( builder.append( '\'' ).toString() ); builder.setLength( 0 ); } else { builder.append( '\'' ); } } else if ( quotedOperand ) { builder.append( operandToken ); } else if ( operandToken.length() == 1 && Character.isWhitespace( operandToken.charAt( 0 ) ) ) { // do nothing } else { operands.add( operandToken ); } operandToken = tokens.nextToken(); hasMoreOperands = tokens.hasMoreTokens() && ! ")".equals( operandToken ); } TrimOperands trimOperands = new TrimOperands( operands ); result.append( "trim(" ); if ( trimOperands.trimSpec != null ) { result.append( trimOperands.trimSpec ).append( ' ' ); } if ( trimOperands.trimChar != null ) { if ( trimOperands.trimChar.startsWith( "'" ) && trimOperands.trimChar.endsWith( "'" ) ) { result.append( trimOperands.trimChar ); } else { result.append( renderWhereStringTemplate( trimOperands.trimSpec, placeholder, dialect, functionRegistry ) ); } result.append( ' ' ); } if ( trimOperands.from != null ) { result.append( trimOperands.from ).append( ' ' ); } else if ( trimOperands.trimSpec != null || trimOperands.trimChar != null ) { // I think ANSI SQL says that the 'from' is not optional if either trim-spec or trim-char are specified result.append( "from " ); } result.append( renderWhereStringTemplate( trimOperands.trimSource, placeholder, dialect, functionRegistry ) ) .append( ')' ); hasMore = tokens.hasMoreTokens(); nextToken = hasMore ? tokens.nextToken() : null; continue; } boolean quotedOrWhitespace = quoted || quotedIdentifier || isQuoteCharacter || Character.isWhitespace( token.charAt(0) ); if ( quotedOrWhitespace ) { result.append( token ); } else if ( beforeTable ) { result.append( token ); beforeTable = false; afterFromTable = true; } else if ( afterFromTable ) { if ( !"as".equals(lcToken) ) { afterFromTable = false; } result.append(token); } else if ( isNamedParameter(token) ) { result.append(token); } else if ( isIdentifier(token, dialect) && !isFunctionOrKeyword(lcToken, nextToken, dialect , functionRegistry) ) { result.append(placeholder) .append('.') .append( dialect.quote(token) ); } else { if ( BEFORE_TABLE_KEYWORDS.contains(lcToken) ) { beforeTable = true; inFromClause = true; } else if ( inFromClause && ",".equals(lcToken) ) { beforeTable = true; } result.append(token); } //Yuck: if ( inFromClause && KEYWORDS.contains( lcToken ) //"as" is not in KEYWORDS && !BEFORE_TABLE_KEYWORDS.contains( lcToken ) ) { inFromClause = false; } } return result.toString(); } // /** // * Takes the where condition provided in the mapping attribute and interpolates the alias. // * Handles sub-selects, quoted identifiers, quoted strings, expressions, SQL functions, // * named parameters. // * // * @param sqlWhereString The string into which to interpolate the placeholder value // * @param placeholder The value to be interpolated into the the sqlWhereString // * @param dialect The dialect to apply // * @param functionRegistry The registry of all sql functions // * // * @return The rendered sql fragment // */ // public static String renderWhereStringTemplate( // String sqlWhereString, // String placeholder, // Dialect dialect, // SQLFunctionRegistry functionRegistry) { // // // IMPL NOTE : The basic process here is to tokenize the incoming string and to iterate over each token // // in turn. As we process each token, we set a series of flags used to indicate the type of context in // // which the tokens occur. Depending on the state of those flags we decide whether we need to qualify // // identifier references. // // final String dialectOpenQuote = Character.toString( dialect.openQuote() ); // final String dialectCloseQuote = Character.toString( dialect.closeQuote() ); // // String symbols = new StringBuilder() // .append( "=><!+-*/()',|&`" ) // .append( StringHelper.WHITESPACE ) // .append( dialect.openQuote() ) // .append( dialect.closeQuote() ) // .toString(); // StringTokenizer tokens = new StringTokenizer( sqlWhereString, symbols, true ); // ProcessingState state = new ProcessingState(); // // StringBuilder quotedBuffer = new StringBuilder(); // StringBuilder result = new StringBuilder(); // // boolean hasMore = tokens.hasMoreTokens(); // String nextToken = hasMore ? tokens.nextToken() : null; // while ( hasMore ) { // String token = nextToken; // String lcToken = token.toLowerCase(); // hasMore = tokens.hasMoreTokens(); // nextToken = hasMore ? tokens.nextToken() : null; // // // First, determine quoting which might be based on either: // // 1) back-tick // // 2) single quote (ANSI SQL standard) // // 3) or dialect defined quote character(s) // QuotingCharacterDisposition quotingCharacterDisposition = QuotingCharacterDisposition.NONE; // if ( "`".equals( token ) ) { // state.quoted = !state.quoted; // quotingCharacterDisposition = state.quoted // ? QuotingCharacterDisposition.OPEN // : QuotingCharacterDisposition.CLOSE; // // replace token with the appropriate dialect quoting char // token = lcToken = ( quotingCharacterDisposition == QuotingCharacterDisposition.OPEN ) // ? dialectOpenQuote // : dialectCloseQuote; // } // else if ( "'".equals( token ) ) { // state.quoted = !state.quoted; // quotingCharacterDisposition = state.quoted // ? QuotingCharacterDisposition.OPEN // : QuotingCharacterDisposition.CLOSE; // } // else if ( !state.quoted && dialectOpenQuote.equals( token ) ) { // state.quoted = true; // quotingCharacterDisposition = QuotingCharacterDisposition.OPEN; // } // else if ( state.quoted && dialectCloseQuote.equals( token ) ) { // state.quoted = false; // quotingCharacterDisposition = QuotingCharacterDisposition.CLOSE; // } // // if ( state.quoted ) { // quotedBuffer.append( token ); // continue; // } // // // if we were previously processing quoted state and just encountered the close quote, then handle that // // quoted text // if ( quotingCharacterDisposition == QuotingCharacterDisposition.CLOSE ) { // token = quotedBuffer.toString(); // quotedBuffer.setLength( 0 ); // result.append( placeholder ).append( '.' ) // .append( dialectOpenQuote ).append( token ).append( dialectCloseQuote ); // continue; // } // // // Special processing for ANSI SQL EXTRACT function // if ( "extract".equals( lcToken ) && "(".equals( nextToken ) ) { // final String field = extractUntil( tokens, "from" ); // final String source = renderWhereStringTemplate( // extractUntil( tokens, ")" ), // placeholder, // dialect, // functionRegistry // ); // result.append( "extract(" ).append( field ).append( " from " ).append( source ).append( ')' ); // // hasMore = tokens.hasMoreTokens(); // nextToken = hasMore ? tokens.nextToken() : null; // // continue; // } // // // Special processing for ANSI SQL TRIM function // if ( "trim".equals( lcToken ) && "(".equals( nextToken ) ) { // List<String> operands = new ArrayList<String>(); // StringBuilder builder = new StringBuilder(); // // boolean hasMoreOperands = true; // String operandToken = tokens.nextToken(); // boolean quoted = false; // while ( hasMoreOperands ) { // final boolean isQuote = "'".equals( operandToken ); // if ( isQuote ) { // quoted = !quoted; // if ( !quoted ) { // operands.add( builder.append( '\'' ).toString() ); // builder.setLength( 0 ); // } // else { // builder.append( '\'' ); // } // } // else if ( quoted ) { // builder.append( operandToken ); // } // else if ( operandToken.length() == 1 && Character.isWhitespace( operandToken.charAt( 0 ) ) ) { // // do nothing // } // else { // operands.add( operandToken ); // } // operandToken = tokens.nextToken(); // hasMoreOperands = tokens.hasMoreTokens() && ! ")".equals( operandToken ); // } // // TrimOperands trimOperands = new TrimOperands( operands ); // result.append( "trim(" ); // if ( trimOperands.trimSpec != null ) { // result.append( trimOperands.trimSpec ).append( ' ' ); // } // if ( trimOperands.trimChar != null ) { // if ( trimOperands.trimChar.startsWith( "'" ) && trimOperands.trimChar.endsWith( "'" ) ) { // result.append( trimOperands.trimChar ); // } // else { // result.append( // renderWhereStringTemplate( trimOperands.trimSpec, placeholder, dialect, functionRegistry ) // ); // } // result.append( ' ' ); // } // if ( trimOperands.from != null ) { // result.append( trimOperands.from ).append( ' ' ); // } // else if ( trimOperands.trimSpec != null || trimOperands.trimChar != null ) { // // I think ANSI SQL says that the 'from' is not optional if either trim-spec or trim-char are specified // result.append( "from " ); // } // // result.append( renderWhereStringTemplate( trimOperands.trimSource, placeholder, dialect, functionRegistry ) ) // .append( ')' ); // // hasMore = tokens.hasMoreTokens(); // nextToken = hasMore ? tokens.nextToken() : null; // // continue; // } // // // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // if ( Character.isWhitespace( token.charAt( 0 ) ) ) { // result.append( token ); // } // else if ( state.beforeTable ) { // result.append( token ); // state.beforeTable = false; // state.afterFromTable = true; // } // else if ( state.afterFromTable ) { // if ( !"as".equals(lcToken) ) { // state.afterFromTable = false; // } // result.append(token); // } // else if ( isNamedParameter(token) ) { // result.append(token); // } // else if ( isIdentifier(token, dialect) // && !isFunctionOrKeyword(lcToken, nextToken, dialect , functionRegistry) ) { // result.append(placeholder) // .append('.') // .append( dialect.quote(token) ); // } // else { // if ( BEFORE_TABLE_KEYWORDS.contains(lcToken) ) { // state.beforeTable = true; // state.inFromClause = true; // } // else if ( state.inFromClause && ",".equals(lcToken) ) { // state.beforeTable = true; // } // result.append(token); // } // // //Yuck: // if ( state.inFromClause // && KEYWORDS.contains( lcToken ) //"as" is not in KEYWORDS // && !BEFORE_TABLE_KEYWORDS.contains( lcToken ) ) { // state.inFromClause = false; // } // } // // return result.toString(); // } // // private static class ProcessingState { // boolean quoted = false; // boolean quotedIdentifier = false; // boolean beforeTable = false; // boolean inFromClause = false; // boolean afterFromTable = false; // } // // private static enum QuotingCharacterDisposition { NONE, OPEN, CLOSE } private static class TrimOperands { private final String trimSpec; private final String trimChar; private final String from; private final String trimSource; private TrimOperands(List<String> operands) { if ( operands.size() == 1 ) { trimSpec = null; trimChar = null; from = null; trimSource = operands.get(0); } else if ( operands.size() == 4 ) { trimSpec = operands.get(0); trimChar = operands.get(1); from = operands.get(2); trimSource = operands.get(3); } else { if ( operands.size() < 1 || operands.size() > 4 ) { throw new HibernateException( "Unexpected number of trim function operands : " + operands.size() ); } // trim-source will always be the last operand trimSource = operands.get( operands.size() - 1 ); // ANSI SQL says that more than one operand means that the FROM is required if ( ! "from".equals( operands.get( operands.size() - 2 ) ) ) { throw new HibernateException( "Expecting FROM, found : " + operands.get( operands.size() - 2 ) ); } from = operands.get( operands.size() - 2 ); // trim-spec, if there is one will always be the first operand if ( "leading".equalsIgnoreCase( operands.get(0) ) || "trailing".equalsIgnoreCase( operands.get(0) ) || "both".equalsIgnoreCase( operands.get(0) ) ) { trimSpec = operands.get(0); trimChar = null; } else { trimSpec = null; if ( operands.size() - 2 == 0 ) { trimChar = null; } else { trimChar = operands.get( 0 ); } } } } } private static String extractUntil(StringTokenizer tokens, String delimiter) { StringBuilder valueBuilder = new StringBuilder(); String token = tokens.nextToken(); while ( ! delimiter.equalsIgnoreCase( token ) ) { valueBuilder.append( token ); token = tokens.nextToken(); } return valueBuilder.toString().trim(); } public static class NoOpColumnMapper implements ColumnMapper { public static final NoOpColumnMapper INSTANCE = new NoOpColumnMapper(); public String[] map(String reference) { return new String[] { reference }; } } /** * Performs order-by template rendering without {@link ColumnMapper column mapping}. An <tt>ORDER BY</tt> template * has all column references "qualified" with a placeholder identified by {@link Template#TEMPLATE} * * @param orderByFragment The order-by fragment to render. * @param dialect The SQL dialect being used. * @param functionRegistry The SQL function registry * * @return The rendered <tt>ORDER BY</tt> template. * * @deprecated Use {@link #renderOrderByStringTemplate(String,ColumnMapper,SessionFactoryImplementor,Dialect,SQLFunctionRegistry)} instead */ @Deprecated public static String renderOrderByStringTemplate( String orderByFragment, Dialect dialect, SQLFunctionRegistry functionRegistry) { return renderOrderByStringTemplate( orderByFragment, NoOpColumnMapper.INSTANCE, null, dialect, functionRegistry ); } /** * Performs order-by template rendering allowing {@link ColumnMapper column mapping}. An <tt>ORDER BY</tt> template * has all column references "qualified" with a placeholder identified by {@link Template#TEMPLATE} which can later * be used to easily inject the SQL alias. * * @param orderByFragment The order-by fragment to render. * @param columnMapper The column mapping strategy to use. * @param sessionFactory The session factory. * @param dialect The SQL dialect being used. * @param functionRegistry The SQL function registry * * @return The rendered <tt>ORDER BY</tt> template. */ public static String renderOrderByStringTemplate( String orderByFragment, final ColumnMapper columnMapper, final SessionFactoryImplementor sessionFactory, final Dialect dialect, final SQLFunctionRegistry functionRegistry) { TranslationContext context = new TranslationContext() { public SessionFactoryImplementor getSessionFactory() { return sessionFactory; } public Dialect getDialect() { return dialect; } public SQLFunctionRegistry getSqlFunctionRegistry() { return functionRegistry; } public ColumnMapper getColumnMapper() { return columnMapper; } }; OrderByFragmentTranslator translator = new OrderByFragmentTranslator( context ); return translator.render( orderByFragment ); } private static boolean isNamedParameter(String token) { return token.startsWith(":"); } private static boolean isFunctionOrKeyword(String lcToken, String nextToken, Dialect dialect, SQLFunctionRegistry functionRegistry) { return "(".equals(nextToken) || KEYWORDS.contains(lcToken) || isFunction(lcToken, nextToken, functionRegistry ) || dialect.getKeywords().contains(lcToken) || FUNCTION_KEYWORDS.contains(lcToken); } private static boolean isFunction(String lcToken, String nextToken, SQLFunctionRegistry functionRegistry) { // checking for "(" is currently redundant because it is checked before getting here; // doing the check anyhow, in case that earlier check goes away; if ( "(".equals( nextToken ) ) { return true; } SQLFunction function = functionRegistry.findSQLFunction(lcToken); if ( function == null ) { // lcToken does not refer to a function return false; } // if function.hasParenthesesIfNoArguments() is true, then assume // lcToken is not a function (since it is not followed by '(') return ! function.hasParenthesesIfNoArguments(); } private static boolean isIdentifier(String token, Dialect dialect) { return token.charAt(0)=='`' || ( //allow any identifier quoted with backtick Character.isLetter( token.charAt(0) ) && //only recognizes identifiers beginning with a letter token.indexOf('.') < 0 ); } }