/* * Hibernate, Relational Persistence for Idiomatic Java * * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.hql.internal.ast; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.QueryException; import org.hibernate.engine.query.spi.EntityGraphQueryHint; import org.hibernate.engine.spi.QueryParameters; import org.hibernate.engine.spi.RowSelection; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; import org.hibernate.hql.internal.QueryExecutionRequestException; import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes; import org.hibernate.hql.internal.antlr.HqlTokenTypes; import org.hibernate.hql.internal.antlr.SqlTokenTypes; import org.hibernate.hql.internal.ast.exec.BasicExecutor; import org.hibernate.hql.internal.ast.exec.DeleteExecutor; import org.hibernate.hql.internal.ast.exec.MultiTableDeleteExecutor; import org.hibernate.hql.internal.ast.exec.MultiTableUpdateExecutor; import org.hibernate.hql.internal.ast.exec.StatementExecutor; import org.hibernate.hql.internal.ast.tree.AggregatedSelectExpression; import org.hibernate.hql.internal.ast.tree.FromElement; import org.hibernate.hql.internal.ast.tree.InsertStatement; import org.hibernate.hql.internal.ast.tree.QueryNode; import org.hibernate.hql.internal.ast.tree.Statement; import org.hibernate.hql.internal.ast.util.ASTPrinter; import org.hibernate.hql.internal.ast.util.ASTUtil; import org.hibernate.hql.internal.ast.util.NodeTraverser; import org.hibernate.hql.spi.FilterTranslator; import org.hibernate.hql.spi.ParameterTranslations; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.ReflectHelper; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.IdentitySet; import org.hibernate.loader.hql.QueryLoader; import org.hibernate.param.ParameterSpecification; import org.hibernate.persister.entity.Queryable; import org.hibernate.query.spi.ScrollableResultsImplementor; import org.hibernate.type.Type; import org.jboss.logging.Logger; import antlr.ANTLRException; import antlr.RecognitionException; import antlr.TokenStreamException; import antlr.collections.AST; /** * A QueryTranslator that uses an Antlr-based parser. * * @author Joshua Davis (pgmjsd@sourceforge.net) */ public class QueryTranslatorImpl implements FilterTranslator { private static final CoreMessageLogger LOG = Logger.getMessageLogger( CoreMessageLogger.class, QueryTranslatorImpl.class.getName() ); private SessionFactoryImplementor factory; private final String queryIdentifier; private String hql; private boolean shallowQuery; private Map tokenReplacements; //TODO:this is only needed during compilation .. can we eliminate the instvar? private Map enabledFilters; private boolean compiled; private QueryLoader queryLoader; private StatementExecutor statementExecutor; private Statement sqlAst; private String sql; private ParameterTranslations paramTranslations; private List<ParameterSpecification> collectedParameterSpecifications; private EntityGraphQueryHint entityGraphQueryHint; /** * Creates a new AST-based query translator. * * @param queryIdentifier The query-identifier (used in stats collection) * @param query The hql query to translate * @param enabledFilters Currently enabled filters * @param factory The session factory constructing this translator instance. */ public QueryTranslatorImpl( String queryIdentifier, String query, Map enabledFilters, SessionFactoryImplementor factory) { this.queryIdentifier = queryIdentifier; this.hql = query; this.compiled = false; this.shallowQuery = false; this.enabledFilters = enabledFilters; this.factory = factory; } public QueryTranslatorImpl( String queryIdentifier, String query, Map enabledFilters, SessionFactoryImplementor factory, EntityGraphQueryHint entityGraphQueryHint) { this( queryIdentifier, query, enabledFilters, factory ); this.entityGraphQueryHint = entityGraphQueryHint; } /** * Compile a "normal" query. This method may be called multiple * times. Subsequent invocations are no-ops. * * @param replacements Defined query substitutions. * @param shallow Does this represent a shallow (scalar or entity-id) select? * @throws QueryException There was a problem parsing the query string. * @throws MappingException There was a problem querying defined mappings. */ @Override public void compile( Map replacements, boolean shallow) throws QueryException, MappingException { doCompile( replacements, shallow, null ); } /** * Compile a filter. This method may be called multiple * times. Subsequent invocations are no-ops. * * @param collectionRole the role name of the collection used as the basis for the filter. * @param replacements Defined query substitutions. * @param shallow Does this represent a shallow (scalar or entity-id) select? * @throws QueryException There was a problem parsing the query string. * @throws MappingException There was a problem querying defined mappings. */ @Override public void compile( String collectionRole, Map replacements, boolean shallow) throws QueryException, MappingException { doCompile( replacements, shallow, collectionRole ); } /** * Performs both filter and non-filter compiling. * * @param replacements Defined query substitutions. * @param shallow Does this represent a shallow (scalar or entity-id) select? * @param collectionRole the role name of the collection used as the basis for the filter, NULL if this * is not a filter. */ private synchronized void doCompile(Map replacements, boolean shallow, String collectionRole) { // If the query is already compiled, skip the compilation. if ( compiled ) { LOG.debug( "compile() : The query is already compiled, skipping..." ); return; } // Remember the parameters for the compilation. this.tokenReplacements = replacements; if ( tokenReplacements == null ) { tokenReplacements = new HashMap(); } this.shallowQuery = shallow; try { // PHASE 1 : Parse the HQL into an AST. final HqlParser parser = parse( true ); // PHASE 2 : Analyze the HQL AST, and produce an SQL AST. final HqlSqlWalker w = analyze( parser, collectionRole ); sqlAst = (Statement) w.getAST(); // at some point the generate phase needs to be moved out of here, // because a single object-level DML might spawn multiple SQL DML // command executions. // // Possible to just move the sql generation for dml stuff, but for // consistency-sake probably best to just move responsiblity for // the generation phase completely into the delegates // (QueryLoader/StatementExecutor) themselves. Also, not sure why // QueryLoader currently even has a dependency on this at all; does // it need it? Ideally like to see the walker itself given to the delegates directly... if ( sqlAst.needsExecutor() ) { statementExecutor = buildAppropriateStatementExecutor( w ); } else { // PHASE 3 : Generate the SQL. generate( (QueryNode) sqlAst ); queryLoader = new QueryLoader( this, factory, w.getSelectClause() ); } compiled = true; } catch ( QueryException qe ) { if ( qe.getQueryString() == null ) { throw qe.wrapWithQueryString( hql ); } else { throw qe; } } catch ( RecognitionException e ) { // we do not actually propagate ANTLRExceptions as a cause, so // log it here for diagnostic purposes LOG.trace( "Converted antlr.RecognitionException", e ); throw QuerySyntaxException.convert( e, hql ); } catch ( ANTLRException e ) { // we do not actually propagate ANTLRExceptions as a cause, so // log it here for diagnostic purposes LOG.trace( "Converted antlr.ANTLRException", e ); throw new QueryException( e.getMessage(), hql ); } catch ( IllegalArgumentException e ) { // translate this into QueryException LOG.trace( "Converted IllegalArgumentException", e ); throw new QueryException( e.getMessage(), hql ); } //only needed during compilation phase... this.enabledFilters = null; } private void generate(AST sqlAst) throws QueryException, RecognitionException { if ( sql == null ) { final SqlGenerator gen = new SqlGenerator( factory ); gen.statement( sqlAst ); sql = gen.getSQL(); if ( LOG.isDebugEnabled() ) { LOG.debugf( "HQL: %s", hql ); LOG.debugf( "SQL: %s", sql ); } gen.getParseErrorHandler().throwQueryException(); collectedParameterSpecifications = gen.getCollectedParameters(); } } private static final ASTPrinter SQL_TOKEN_PRINTER = new ASTPrinter( SqlTokenTypes.class ); private HqlSqlWalker analyze(HqlParser parser, String collectionRole) throws QueryException, RecognitionException { final HqlSqlWalker w = new HqlSqlWalker( this, factory, parser, tokenReplacements, collectionRole ); final AST hqlAst = parser.getAST(); // Transform the tree. w.statement( hqlAst ); if ( LOG.isDebugEnabled() ) { LOG.debug( SQL_TOKEN_PRINTER.showAsString( w.getAST(), "--- SQL AST ---" ) ); } w.getParseErrorHandler().throwQueryException(); return w; } private HqlParser parse(boolean filter) throws TokenStreamException, RecognitionException { // Parse the query string into an HQL AST. final HqlParser parser = HqlParser.getInstance( hql ); parser.setFilter( filter ); LOG.debugf( "parse() - HQL: %s", hql ); parser.statement(); final AST hqlAst = parser.getAST(); final NodeTraverser walker = new NodeTraverser( new JavaConstantConverter( factory ) ); walker.traverseDepthFirst( hqlAst ); showHqlAst( hqlAst ); parser.getParseErrorHandler().throwQueryException(); return parser; } private static final ASTPrinter HQL_TOKEN_PRINTER = new ASTPrinter( HqlTokenTypes.class ); void showHqlAst(AST hqlAst) { if ( LOG.isDebugEnabled() ) { LOG.debug( HQL_TOKEN_PRINTER.showAsString( hqlAst, "--- HQL AST ---" ) ); } } private void errorIfDML() throws HibernateException { if ( sqlAst.needsExecutor() ) { throw new QueryExecutionRequestException( "Not supported for DML operations", hql ); } } private void errorIfSelect() throws HibernateException { if ( !sqlAst.needsExecutor() ) { throw new QueryExecutionRequestException( "Not supported for select queries", hql ); } } @Override public String getQueryIdentifier() { return queryIdentifier; } public Statement getSqlAST() { return sqlAst; } private HqlSqlWalker getWalker() { return sqlAst.getWalker(); } /** * Types of the return values of an <tt>iterate()</tt> style query. * * @return an array of <tt>Type</tt>s. */ @Override public Type[] getReturnTypes() { errorIfDML(); return getWalker().getReturnTypes(); } @Override public String[] getReturnAliases() { errorIfDML(); return getWalker().getReturnAliases(); } @Override public String[][] getColumnNames() { errorIfDML(); return getWalker().getSelectClause().getColumnNames(); } @Override public Set<Serializable> getQuerySpaces() { return getWalker().getQuerySpaces(); } @Override public List list(SharedSessionContractImplementor session, QueryParameters queryParameters) throws HibernateException { // Delegate to the QueryLoader... errorIfDML(); final QueryNode query = (QueryNode) sqlAst; final boolean hasLimit = queryParameters.getRowSelection() != null && queryParameters.getRowSelection().definesLimits(); final boolean needsDistincting = ( query.getSelectClause().isDistinct() || getEntityGraphQueryHint() != null || hasLimit ) && containsCollectionFetches(); QueryParameters queryParametersToUse; if ( hasLimit && containsCollectionFetches() ) { LOG.firstOrMaxResultsSpecifiedWithCollectionFetch(); RowSelection selection = new RowSelection(); selection.setFetchSize( queryParameters.getRowSelection().getFetchSize() ); selection.setTimeout( queryParameters.getRowSelection().getTimeout() ); queryParametersToUse = queryParameters.createCopyUsing( selection ); } else { queryParametersToUse = queryParameters; } List results = queryLoader.list( session, queryParametersToUse ); if ( needsDistincting ) { int includedCount = -1; // NOTE : firstRow is zero-based int first = !hasLimit || queryParameters.getRowSelection().getFirstRow() == null ? 0 : queryParameters.getRowSelection().getFirstRow(); int max = !hasLimit || queryParameters.getRowSelection().getMaxRows() == null ? -1 : queryParameters.getRowSelection().getMaxRows(); List tmp = new ArrayList(); IdentitySet distinction = new IdentitySet(); for ( final Object result : results ) { if ( !distinction.add( result ) ) { continue; } includedCount++; if ( includedCount < first ) { continue; } tmp.add( result ); // NOTE : ( max - 1 ) because first is zero-based while max is not... if ( max >= 0 && ( includedCount - first ) >= ( max - 1 ) ) { break; } } results = tmp; } return results; } /** * Return the query results as an iterator */ @Override public Iterator iterate(QueryParameters queryParameters, EventSource session) throws HibernateException { // Delegate to the QueryLoader... errorIfDML(); return queryLoader.iterate( queryParameters, session ); } /** * Return the query results, as an instance of <tt>ScrollableResults</tt> */ @Override public ScrollableResultsImplementor scroll(QueryParameters queryParameters, SharedSessionContractImplementor session) throws HibernateException { // Delegate to the QueryLoader... errorIfDML(); return queryLoader.scroll( queryParameters, session ); } @Override public int executeUpdate(QueryParameters queryParameters, SharedSessionContractImplementor session) throws HibernateException { errorIfSelect(); return statementExecutor.execute( queryParameters, session ); } /** * The SQL query string to be called; implemented by all subclasses */ @Override public String getSQLString() { return sql; } @Override public List<String> collectSqlStrings() { ArrayList<String> list = new ArrayList<String>(); if ( isManipulationStatement() ) { String[] sqlStatements = statementExecutor.getSqlStatements(); Collections.addAll( list, sqlStatements ); } else { list.add( sql ); } return list; } // -- Package local methods for the QueryLoader delegate -- public boolean isShallowQuery() { return shallowQuery; } @Override public String getQueryString() { return hql; } @Override public Map getEnabledFilters() { return enabledFilters; } public int[] getNamedParameterLocs(String name) { return getWalker().getNamedParameterLocations( name ); } @Override public boolean containsCollectionFetches() { errorIfDML(); List collectionFetches = ( (QueryNode) sqlAst ).getFromClause().getCollectionFetches(); return collectionFetches != null && collectionFetches.size() > 0; } @Override public boolean isManipulationStatement() { return sqlAst.needsExecutor(); } @Override public void validateScrollability() throws HibernateException { // Impl Note: allows multiple collection fetches as long as the // entire fecthed graph still "points back" to a single // root entity for return errorIfDML(); final QueryNode query = (QueryNode) sqlAst; // If there are no collection fetches, then no further checks are needed List collectionFetches = query.getFromClause().getCollectionFetches(); if ( collectionFetches.isEmpty() ) { return; } // A shallow query is ok (although technically there should be no fetching here...) if ( isShallowQuery() ) { return; } // Otherwise, we have a non-scalar select with defined collection fetch(es). // Make sure that there is only a single root entity in the return (no tuples) if ( getReturnTypes().length > 1 ) { throw new HibernateException( "cannot scroll with collection fetches and returned tuples" ); } FromElement owner = null; for ( Object o : query.getSelectClause().getFromElementsForLoad() ) { // should be the first, but just to be safe... final FromElement fromElement = (FromElement) o; if ( fromElement.getOrigin() == null ) { owner = fromElement; break; } } if ( owner == null ) { throw new HibernateException( "unable to locate collection fetch(es) owner for scrollability checks" ); } // This is not strictly true. We actually just need to make sure that // it is ordered by root-entity PK and that that order-by comes before // any non-root-entity ordering... AST primaryOrdering = query.getOrderByClause().getFirstChild(); if ( primaryOrdering != null ) { // TODO : this is a bit dodgy, come up with a better way to check this (plus see above comment) String [] idColNames = owner.getQueryable().getIdentifierColumnNames(); String expectedPrimaryOrderSeq = StringHelper.join( ", ", StringHelper.qualify( owner.getTableAlias(), idColNames ) ); if ( !primaryOrdering.getText().startsWith( expectedPrimaryOrderSeq ) ) { throw new HibernateException( "cannot scroll results with collection fetches which are not ordered primarily by the root entity's PK" ); } } } private StatementExecutor buildAppropriateStatementExecutor(HqlSqlWalker walker) { final Statement statement = (Statement) walker.getAST(); if ( walker.getStatementType() == HqlSqlTokenTypes.DELETE ) { final FromElement fromElement = walker.getFinalFromClause().getFromElement(); final Queryable persister = fromElement.getQueryable(); if ( persister.isMultiTable() ) { return new MultiTableDeleteExecutor( walker ); } else { return new DeleteExecutor( walker, persister ); } } else if ( walker.getStatementType() == HqlSqlTokenTypes.UPDATE ) { final FromElement fromElement = walker.getFinalFromClause().getFromElement(); final Queryable persister = fromElement.getQueryable(); if ( persister.isMultiTable() ) { // even here, if only properties mapped to the "base table" are referenced // in the set and where clauses, this could be handled by the BasicDelegate. // TODO : decide if it is better performance-wise to doAfterTransactionCompletion that check, or to simply use the MultiTableUpdateDelegate return new MultiTableUpdateExecutor( walker ); } else { return new BasicExecutor( walker, persister ); } } else if ( walker.getStatementType() == HqlSqlTokenTypes.INSERT ) { return new BasicExecutor( walker, ( (InsertStatement) statement ).getIntoClause().getQueryable() ); } else { throw new QueryException( "Unexpected statement type" ); } } @Override public ParameterTranslations getParameterTranslations() { if ( paramTranslations == null ) { paramTranslations = new ParameterTranslationsImpl( getWalker().getParameters() ); } return paramTranslations; } public List<ParameterSpecification> getCollectedParameterSpecifications() { return collectedParameterSpecifications; } @Override public Class getDynamicInstantiationResultType() { AggregatedSelectExpression aggregation = queryLoader.getAggregatedSelectExpression(); return aggregation == null ? null : aggregation.getAggregationResultType(); } public static class JavaConstantConverter implements NodeTraverser.VisitationStrategy { private final SessionFactoryImplementor factory; private AST dotRoot; public JavaConstantConverter(SessionFactoryImplementor factory) { this.factory = factory; } @Override public void visit(AST node) { if ( dotRoot != null ) { // we are already processing a dot-structure if ( ASTUtil.isSubtreeChild( dotRoot, node ) ) { return; } // we are now at a new tree level dotRoot = null; } if ( node.getType() == HqlTokenTypes.DOT ) { dotRoot = node; handleDotStructure( dotRoot ); } } private void handleDotStructure(AST dotStructureRoot) { final String expression = ASTUtil.getPathText( dotStructureRoot ); final Object constant = ReflectHelper.getConstantValue( expression, factory ); if ( constant != null ) { dotStructureRoot.setFirstChild( null ); dotStructureRoot.setType( HqlTokenTypes.JAVA_CONSTANT ); dotStructureRoot.setText( expression ); } } } public EntityGraphQueryHint getEntityGraphQueryHint() { return entityGraphQueryHint; } public void setEntityGraphQueryHint(EntityGraphQueryHint entityGraphQueryHint) { this.entityGraphQueryHint = entityGraphQueryHint; } }