diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/pagination/SQLServer2005LimitHandler.java b/hibernate-core/src/main/java/org/hibernate/dialect/pagination/SQLServer2005LimitHandler.java index 722f395d9c..0ff2062e15 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/pagination/SQLServer2005LimitHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/pagination/SQLServer2005LimitHandler.java @@ -8,6 +8,7 @@ package org.hibernate.dialect.pagination; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; @@ -316,11 +317,28 @@ public class SQLServer2005LimitHandler extends AbstractLimitHandler { return -1; } + List ignoreRangeList = generateIgnoreRanges( matchString ); + Matcher matcher = pattern.matcher( matchString ); matcher.region( fromIndex, matchString.length() ); - if ( matcher.find() && matcher.groupCount() > 0 ) { - index = matcher.start(); + if ( ignoreRangeList.isEmpty() ) { + // old behavior where the first match is used if no ignorable ranges + // were deduced from the matchString. + if ( matcher.find() && matcher.groupCount() > 0 ) { + index = matcher.start(); + } + } + else { + // rather than taking the first match, we now iterate all matches + // until we determine a match that isn't considered "ignorable'. + while ( matcher.find() && matcher.groupCount() > 0 ) { + final int position = matcher.start(); + if ( !isPositionIgnorable( ignoreRangeList, position ) ) { + index = position; + break; + } + } } return index; } @@ -342,4 +360,75 @@ public class SQLServer2005LimitHandler extends AbstractLimitHandler { Pattern.CASE_INSENSITIVE ); } + + /** + * Geneartes a list of {@code IgnoreRange} objects that represent nested sections of the + * provided SQL buffer that should be ignored when performing regular expression matches. + * + * @param sql The SQL buffer. + * @return list of {@code IgnoreRange} objects, never {@code null}. + */ + private static List generateIgnoreRanges(String sql) { + List ignoreRangeList = new ArrayList(); + + int depth = 0; + int start = -1; + for ( int i = 0; i < sql.length(); ++i ) { + final char ch = sql.charAt( i ); + if ( ch == '(' ) { + depth++; + if ( depth == 1 ) { + start = i; + } + } + else if ( ch == ')' ) { + if ( depth > 0 ) { + if ( depth == 1 ) { + ignoreRangeList.add( new IgnoreRange( start, i ) ); + start = -1; + } + depth--; + } + else { + throw new IllegalStateException( "Found an unmatched ')' at position " + i + ": " + sql ); + } + } + } + + if ( depth != 0 ) { + throw new IllegalStateException( "Unmatched parenthesis in rendered SQL (" + depth + " depth): " + sql ); + } + + return ignoreRangeList; + } + + /** + * Returns whether the specified {@code position} is within the ranges of the {@code ignoreRangeList}. + * + * @param ignoreRangeList list of {@code IgnoreRange} objects deduced from the SQL buffer. + * @param position the position to determine whether is ignorable. + * @return {@code true} if the position is to ignored/skipped, {@code false} otherwise. + */ + private static boolean isPositionIgnorable(List ignoreRangeList, int position) { + for ( IgnoreRange ignoreRange : ignoreRangeList ) { + if ( ignoreRange.isWithinRange( position ) ) { + return true; + } + } + return false; + } + + static class IgnoreRange { + private int start; + private int end; + + IgnoreRange(int start, int end) { + this.start = start; + this.end = end; + } + + boolean isWithinRange(int position) { + return position >= start && position <= end; + } + } }