blob: 17656b03b8f2eba47b1f5b0d75ae6e543b59847b [file] [log] [blame]
/*
* 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();
}
}