/* * 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.sql.ordering.antlr; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.hibernate.dialect.function.SQLFunction; import org.hibernate.internal.util.StringHelper; import org.hibernate.sql.Template; import org.jboss.logging.Logger; import antlr.CommonAST; import antlr.TokenStream; import antlr.collections.AST; /** * Extension of the Antlr-generated parser for the purpose of adding our custom parsing behavior * (semantic analysis, etc). * * @author Steve Ebersole */ public class OrderByFragmentParser extends GeneratedOrderByFragmentParser { private static final Logger LOG = Logger.getLogger( OrderByFragmentParser.class.getName() ); private final TranslationContext context; private Set<String> columnReferences = new HashSet<String>(); public OrderByFragmentParser(TokenStream lexer, TranslationContext context) { super( lexer ); super.setASTFactory( new Factory() ); this.context = context; } public Set<String> getColumnReferences() { return columnReferences; } @Override protected AST quotedIdentifier(AST ident) { /* * Semantic action used during recognition of quoted identifiers (quoted column names) */ final String columnName = context.getDialect().quote( '`' + ident.getText() + '`' ); columnReferences.add( columnName ); final String marker = '{' + columnName + '}'; return getASTFactory().create( OrderByTemplateTokenTypes.IDENT, marker ); } @Override protected AST quotedString(AST ident) { /* * Semantic action used during recognition of quoted strings (string literals) */ return getASTFactory().create( OrderByTemplateTokenTypes.IDENT, context.getDialect().quote( ident.getText() ) ); } @Override @SuppressWarnings("SimplifiableIfStatement") protected boolean isFunctionName(AST ast) { /* * Semantic predicate used to determine whether a given AST node represents a function call */ AST child = ast.getFirstChild(); // assume it is a function if it has parameters if ( child != null && "{param list}".equals( child.getText() ) ) { return true; } // otherwise, in order for this to be a function logically it has to be a function that does not // have arguments. So try to assert that using the registry of known functions final SQLFunction function = context.getSqlFunctionRegistry().findSQLFunction( ast.getText() ); if ( function == null ) { // no registered function, so we cannot know for certain return false; } else { // if function.hasParenthesesIfNoArguments() is true, then assume the node is not a function return !function.hasParenthesesIfNoArguments(); } } @SuppressWarnings("unchecked") @Override protected AST resolveFunction(AST ast) { /* * Semantic action used during recognition of a *known* function */ AST child = ast.getFirstChild(); if ( child != null ) { assert "{param list}".equals( child.getText() ); child = child.getFirstChild(); } final String functionName = ast.getText(); final SQLFunction function = context.getSqlFunctionRegistry().findSQLFunction( functionName ); if ( function == null ) { String text = functionName; if ( child != null ) { text += '('; while ( child != null ) { text += resolveFunctionArgument( child ); child = child.getNextSibling(); if ( child != null ) { text += ", "; } } text += ')'; } return getASTFactory().create( OrderByTemplateTokenTypes.IDENT, text ); } else { ArrayList expressions = new ArrayList(); while ( child != null ) { expressions.add( resolveFunctionArgument( child ) ); child = child.getNextSibling(); } final String text = function.render( null, expressions, context.getSessionFactory() ); return getASTFactory().create( OrderByTemplateTokenTypes.IDENT, text ); } } private String resolveFunctionArgument(AST argumentNode) { final String nodeText = argumentNode.getText(); final String adjustedText; if ( nodeText.contains( Template.TEMPLATE ) ) { // we have a SQL order-by fragment adjustedText = adjustTemplateReferences( nodeText ); } else if ( nodeText.startsWith( "{" ) && nodeText.endsWith( "}" ) ) { columnReferences.add( nodeText.substring( 1, nodeText.length() - 1 ) ); return nodeText; } else { adjustedText = nodeText; // because we did not process the node text, we need to attempt to find any column references // contained in it. // NOTE : uses regex for the time being; we should check the performance of this Pattern pattern = Pattern.compile( "\\{(.*)\\}" ); Matcher matcher = pattern.matcher( adjustedText ); while ( matcher.find() ) { columnReferences.add( matcher.group( 1 ) ); } } return adjustedText; } @Override protected AST resolveIdent(AST ident) { /* * Semantic action used during recognition of an identifier. This identifier might be a column name, it might * be a property name. */ String text = ident.getText(); SqlValueReference[] sqlValueReferences; try { sqlValueReferences = context.getColumnMapper().map( text ); } catch (Throwable t) { sqlValueReferences = null; } if ( sqlValueReferences == null || sqlValueReferences.length == 0 ) { return getASTFactory().create( OrderByTemplateTokenTypes.IDENT, makeColumnReference( text ) ); } else if ( sqlValueReferences.length == 1 ) { return processSqlValueReference( sqlValueReferences[0] ); } else { final AST root = getASTFactory().create( OrderByTemplateTokenTypes.IDENT_LIST, "{ident list}" ); for ( SqlValueReference sqlValueReference : sqlValueReferences ) { root.addChild( processSqlValueReference( sqlValueReference ) ); } return root; } } private AST processSqlValueReference(SqlValueReference sqlValueReference) { if ( ColumnReference.class.isInstance( sqlValueReference ) ) { final String columnName = ( (ColumnReference) sqlValueReference ).getColumnName(); return getASTFactory().create( OrderByTemplateTokenTypes.IDENT, makeColumnReference( columnName ) ); } else { final String formulaFragment = ( (FormulaReference) sqlValueReference ).getFormulaFragment(); // formulas have already been "adjusted" for aliases by appending Template.TEMPLATE to places // where we believe column references are. Fixing that is beyond scope of this work. But we need // to re-adjust that to use the order-by expectation of wrapping the column names in curly // braces (i.e., `{column_name}`). final String adjustedText = adjustTemplateReferences( formulaFragment ); return getASTFactory().create( OrderByTemplateTokenTypes.IDENT, adjustedText ); } } private String makeColumnReference(String text) { columnReferences.add( text ); return "{" + text + "}"; } private static final int TEMPLATE_MARKER_LENGTH = Template.TEMPLATE.length(); private String adjustTemplateReferences(String template) { int templateLength = template.length(); int startPos = template.indexOf( Template.TEMPLATE ); while ( startPos != -1 && startPos < templateLength ) { int dotPos = startPos + TEMPLATE_MARKER_LENGTH; // from here we need to seek the end of the qualified identifier int pos = dotPos + 1; while ( pos < templateLength && isValidIdentifierCharacter( template.charAt( pos ) ) ) { pos++; } // At this point we know all 3 points in the template that are needed for replacement. // Basically we will be replacing the whole match with the bit following the dot, but will wrap // the replacement in curly braces. final String columnReference = template.substring( dotPos + 1, pos ); final String replacement = "{" + columnReference + "}"; template = template.replace( template.substring( startPos, pos ), replacement ); columnReferences.add( columnReference ); // prep for the next seek startPos = template.indexOf( Template.TEMPLATE, ( pos - TEMPLATE_MARKER_LENGTH ) + 1 ); templateLength = template.length(); } return template; } private static boolean isValidIdentifierCharacter(char c) { return Character.isLetter( c ) || Character.isDigit( c ) || '_' == c || '\"' == c; } @Override protected AST postProcessSortSpecification(AST sortSpec) { assert SORT_SPEC == sortSpec.getType(); SortSpecification sortSpecification = (SortSpecification) sortSpec; AST sortKey = sortSpecification.getSortKey(); if ( IDENT_LIST == sortKey.getFirstChild().getType() ) { AST identList = sortKey.getFirstChild(); AST ident = identList.getFirstChild(); AST holder = new CommonAST(); do { holder.addChild( createSortSpecification( ident, sortSpecification.getCollation(), sortSpecification.getOrdering() ) ); ident = ident.getNextSibling(); } while ( ident != null ); sortSpec = holder.getFirstChild(); } return sortSpec; } private SortSpecification createSortSpecification( AST ident, CollationSpecification collationSpecification, OrderingSpecification orderingSpecification) { AST sortSpecification = getASTFactory().create( SORT_SPEC, "{{sort specification}}" ); AST sortKey = getASTFactory().create( SORT_KEY, "{{sort key}}" ); AST newIdent = getASTFactory().create( ident.getType(), ident.getText() ); sortKey.setFirstChild( newIdent ); sortSpecification.setFirstChild( sortKey ); if ( collationSpecification != null ) { sortSpecification.addChild( collationSpecification ); } if ( orderingSpecification != null ) { sortSpecification.addChild( orderingSpecification ); } return (SortSpecification) sortSpecification; } // trace logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private int traceDepth = 0; @Override public void traceIn(String ruleName) { if ( inputState.guessing > 0 ) { return; } String prefix = StringHelper.repeat( '-', ( traceDepth++ * 2 ) ) + "-> "; LOG.trace( prefix + ruleName ); } @Override public void traceOut(String ruleName) { if ( inputState.guessing > 0 ) { return; } String prefix = "<-" + StringHelper.repeat( '-', ( --traceDepth * 2 ) ) + " "; LOG.trace( prefix + ruleName ); } @Override protected void trace(String msg) { LOG.trace( msg ); } }