| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package groovy.sql; |
| |
| import groovy.lang.Tuple; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Extracts and indexes named parameters from a sql string. |
| * |
| * This class is package-private scoped and is only intended for internal use. |
| * |
| * @see groovy.sql.Sql |
| */ |
| class ExtractIndexAndSql { |
| |
| private static final Pattern NAMED_QUERY_PATTERN = Pattern.compile("(?<!:)(:)(\\w+)|\\?(\\d*)(?:\\.(\\w+))?"); |
| private static final char QUOTE = '\''; |
| |
| private final String sql; |
| private List<Tuple<?>> indexPropList; |
| private String newSql; |
| |
| /** |
| * Used to track the current position within the sql while parsing |
| */ |
| private int index = 0; |
| |
| /** |
| * Static factory method used to create a new instance. Since parsing of the input |
| * is required, this ensures the object is fully initialized. |
| * |
| * @param sql statement to be parsed |
| * @return an instance of {@link ExtractIndexAndSql} |
| */ |
| static ExtractIndexAndSql from(String sql) { |
| return new ExtractIndexAndSql(sql).invoke(); |
| } |
| |
| /** |
| * Checks a sql statement to determine whether it contains parameters. |
| * |
| * @param sql statement |
| * @return {@code true} if the statement contains named parameters, otherwise {@code false} |
| */ |
| static boolean hasNamedParameters(String sql) { |
| return NAMED_QUERY_PATTERN.matcher(sql).find(); |
| } |
| |
| private ExtractIndexAndSql(String sql) { |
| this.sql = sql; |
| } |
| |
| List<Tuple<?>> getIndexPropList() { |
| return indexPropList; |
| } |
| |
| String getNewSql() { |
| return newSql; |
| } |
| |
| private ExtractIndexAndSql invoke() { |
| indexPropList = new ArrayList<>(); |
| StringBuilder sb = new StringBuilder(); |
| StringBuilder currentChunk = new StringBuilder(); |
| while (index < sql.length()) { |
| switch (sql.charAt(index)) { |
| case QUOTE: |
| sb.append(adaptForNamedParams(currentChunk.toString(), indexPropList)); |
| currentChunk = new StringBuilder(); |
| appendToEndOfString(sb); |
| break; |
| case '-': |
| if (next() == '-') { |
| sb.append(adaptForNamedParams(currentChunk.toString(), indexPropList)); |
| currentChunk = new StringBuilder(); |
| appendToEndOfLine(sb); |
| } else { |
| currentChunk.append(sql.charAt(index)); |
| } |
| break; |
| case '/': |
| if (next() == '*') { |
| sb.append(adaptForNamedParams(currentChunk.toString(), indexPropList)); |
| currentChunk = new StringBuilder(); |
| appendToEndOfComment(sb); |
| } else { |
| currentChunk.append(sql.charAt(index)); |
| } |
| break; |
| default: |
| currentChunk.append(sql.charAt(index)); |
| } |
| index++; |
| } |
| sb.append(adaptForNamedParams(currentChunk.toString(), indexPropList)); |
| newSql = sb.toString(); |
| return this; |
| } |
| |
| private void appendToEndOfString(StringBuilder buffer) { |
| buffer.append(QUOTE); |
| int startQuoteIndex = index; |
| ++index; |
| boolean foundClosingQuote = false; |
| while (index < sql.length()) { |
| char c = sql.charAt(index); |
| buffer.append(c); |
| if (c == QUOTE && next() != QUOTE) { |
| if (startQuoteIndex == (index - 1)) { // empty quote '' |
| foundClosingQuote = true; |
| break; |
| } |
| int previousQuotes = countPreviousRepeatingChars(QUOTE); |
| if (previousQuotes == 0 || |
| (previousQuotes % 2 == 0 && (index - previousQuotes) != startQuoteIndex) || |
| (previousQuotes % 2 != 0 && (index - previousQuotes) == startQuoteIndex)) { |
| foundClosingQuote = true; |
| break; |
| } |
| } |
| ++index; |
| } |
| if (!foundClosingQuote) { |
| throw new IllegalStateException("Failed to process query. Unterminated ' character?"); |
| } |
| } |
| |
| private int countPreviousRepeatingChars(char c) { |
| int pos = index - 1; |
| while (pos >= 0) { |
| if (sql.charAt(pos) != c) { |
| break; |
| } |
| --pos; |
| } |
| return (index - 1) - pos; |
| } |
| |
| private void appendToEndOfComment(StringBuilder buffer) { |
| while (index < sql.length()) { |
| char c = sql.charAt(index); |
| buffer.append(c); |
| if (c == '*' && next() == '/') { |
| buffer.append('/'); |
| ++index; |
| break; |
| } |
| ++index; |
| } |
| } |
| |
| private void appendToEndOfLine(StringBuilder buffer) { |
| while (index < sql.length()) { |
| char c = sql.charAt(index); |
| buffer.append(c); |
| if (c == '\n' || c == '\r') { |
| break; |
| } |
| ++index; |
| } |
| } |
| |
| private char next() { |
| return ((index + 1) < sql.length()) ? sql.charAt(index + 1) : '\0'; |
| } |
| |
| private static String adaptForNamedParams(String sql, List<Tuple<?>> indexPropList) { |
| StringBuilder newSql = new StringBuilder(); |
| int txtIndex = 0; |
| |
| Matcher matcher = NAMED_QUERY_PATTERN.matcher(sql); |
| while (matcher.find()) { |
| newSql.append(sql, txtIndex, matcher.start()).append('?'); |
| String indexStr = matcher.group(1); |
| if (indexStr == null) indexStr = matcher.group(3); |
| int index = (indexStr == null || indexStr.length() == 0 || ":".equals(indexStr)) ? 0 : Integer.parseInt(indexStr) - 1; |
| String prop = matcher.group(2); |
| if (prop == null) prop = matcher.group(4); |
| indexPropList.add(new Tuple<Object>(index, prop == null || prop.length() == 0 ? "<this>" : prop)); |
| txtIndex = matcher.end(); |
| } |
| newSql.append(sql, txtIndex, sql.length()); // append ending SQL after last param. |
| return newSql.toString(); |
| } |
| |
| } |