001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.jdbc.core.namedparam; 018 019import java.util.ArrayList; 020import java.util.HashSet; 021import java.util.Iterator; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025 026import org.springframework.dao.InvalidDataAccessApiUsageException; 027import org.springframework.jdbc.core.SqlParameter; 028import org.springframework.jdbc.core.SqlParameterValue; 029import org.springframework.lang.Nullable; 030import org.springframework.util.Assert; 031 032/** 033 * Helper methods for named parameter parsing. 034 * 035 * <p>Only intended for internal use within Spring's JDBC framework. 036 * 037 * @author Thomas Risberg 038 * @author Juergen Hoeller 039 * @since 2.0 040 */ 041public abstract class NamedParameterUtils { 042 043 /** 044 * Set of characters that qualify as comment or quotes starting characters. 045 */ 046 private static final String[] START_SKIP = new String[] {"'", "\"", "--", "/*"}; 047 048 /** 049 * Set of characters that at are the corresponding comment or quotes ending characters. 050 */ 051 private static final String[] STOP_SKIP = new String[] {"'", "\"", "\n", "*/"}; 052 053 /** 054 * Set of characters that qualify as parameter separators, 055 * indicating that a parameter name in an SQL String has ended. 056 */ 057 private static final String PARAMETER_SEPARATORS = "\"':&,;()|=+-*%/\\<>^"; 058 059 /** 060 * An index with separator flags per character code. 061 * Technically only needed between 34 and 124 at this point. 062 */ 063 private static final boolean[] separatorIndex = new boolean[128]; 064 065 static { 066 for (char c : PARAMETER_SEPARATORS.toCharArray()) { 067 separatorIndex[c] = true; 068 } 069 } 070 071 072 //------------------------------------------------------------------------- 073 // Core methods used by NamedParameterJdbcTemplate and SqlQuery/SqlUpdate 074 //------------------------------------------------------------------------- 075 076 /** 077 * Parse the SQL statement and locate any placeholders or named parameters. 078 * Named parameters are substituted for a JDBC placeholder. 079 * @param sql the SQL statement 080 * @return the parsed statement, represented as ParsedSql instance 081 */ 082 public static ParsedSql parseSqlStatement(final String sql) { 083 Assert.notNull(sql, "SQL must not be null"); 084 085 Set<String> namedParameters = new HashSet<>(); 086 String sqlToUse = sql; 087 List<ParameterHolder> parameterList = new ArrayList<>(); 088 089 char[] statement = sql.toCharArray(); 090 int namedParameterCount = 0; 091 int unnamedParameterCount = 0; 092 int totalParameterCount = 0; 093 094 int escapes = 0; 095 int i = 0; 096 while (i < statement.length) { 097 int skipToPosition = i; 098 while (i < statement.length) { 099 skipToPosition = skipCommentsAndQuotes(statement, i); 100 if (i == skipToPosition) { 101 break; 102 } 103 else { 104 i = skipToPosition; 105 } 106 } 107 if (i >= statement.length) { 108 break; 109 } 110 char c = statement[i]; 111 if (c == ':' || c == '&') { 112 int j = i + 1; 113 if (c == ':' && j < statement.length && statement[j] == ':') { 114 // Postgres-style "::" casting operator should be skipped 115 i = i + 2; 116 continue; 117 } 118 String parameter = null; 119 if (c == ':' && j < statement.length && statement[j] == '{') { 120 // :{x} style parameter 121 while (statement[j] != '}') { 122 j++; 123 if (j >= statement.length) { 124 throw new InvalidDataAccessApiUsageException("Non-terminated named parameter declaration " + 125 "at position " + i + " in statement: " + sql); 126 } 127 if (statement[j] == ':' || statement[j] == '{') { 128 throw new InvalidDataAccessApiUsageException("Parameter name contains invalid character '" + 129 statement[j] + "' at position " + i + " in statement: " + sql); 130 } 131 } 132 if (j - i > 2) { 133 parameter = sql.substring(i + 2, j); 134 namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter); 135 totalParameterCount = addNamedParameter( 136 parameterList, totalParameterCount, escapes, i, j + 1, parameter); 137 } 138 j++; 139 } 140 else { 141 while (j < statement.length && !isParameterSeparator(statement[j])) { 142 j++; 143 } 144 if (j - i > 1) { 145 parameter = sql.substring(i + 1, j); 146 namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter); 147 totalParameterCount = addNamedParameter( 148 parameterList, totalParameterCount, escapes, i, j, parameter); 149 } 150 } 151 i = j - 1; 152 } 153 else { 154 if (c == '\\') { 155 int j = i + 1; 156 if (j < statement.length && statement[j] == ':') { 157 // escaped ":" should be skipped 158 sqlToUse = sqlToUse.substring(0, i - escapes) + sqlToUse.substring(i - escapes + 1); 159 escapes++; 160 i = i + 2; 161 continue; 162 } 163 } 164 if (c == '?') { 165 int j = i + 1; 166 if (j < statement.length && (statement[j] == '?' || statement[j] == '|' || statement[j] == '&')) { 167 // Postgres-style "??", "?|", "?&" operator should be skipped 168 i = i + 2; 169 continue; 170 } 171 unnamedParameterCount++; 172 totalParameterCount++; 173 } 174 } 175 i++; 176 } 177 ParsedSql parsedSql = new ParsedSql(sqlToUse); 178 for (ParameterHolder ph : parameterList) { 179 parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex()); 180 } 181 parsedSql.setNamedParameterCount(namedParameterCount); 182 parsedSql.setUnnamedParameterCount(unnamedParameterCount); 183 parsedSql.setTotalParameterCount(totalParameterCount); 184 return parsedSql; 185 } 186 187 private static int addNamedParameter( 188 List<ParameterHolder> parameterList, int totalParameterCount, int escapes, int i, int j, String parameter) { 189 190 parameterList.add(new ParameterHolder(parameter, i - escapes, j - escapes)); 191 totalParameterCount++; 192 return totalParameterCount; 193 } 194 195 private static int addNewNamedParameter(Set<String> namedParameters, int namedParameterCount, String parameter) { 196 if (!namedParameters.contains(parameter)) { 197 namedParameters.add(parameter); 198 namedParameterCount++; 199 } 200 return namedParameterCount; 201 } 202 203 /** 204 * Skip over comments and quoted names present in an SQL statement. 205 * @param statement character array containing SQL statement 206 * @param position current position of statement 207 * @return next position to process after any comments or quotes are skipped 208 */ 209 private static int skipCommentsAndQuotes(char[] statement, int position) { 210 for (int i = 0; i < START_SKIP.length; i++) { 211 if (statement[position] == START_SKIP[i].charAt(0)) { 212 boolean match = true; 213 for (int j = 1; j < START_SKIP[i].length(); j++) { 214 if (statement[position + j] != START_SKIP[i].charAt(j)) { 215 match = false; 216 break; 217 } 218 } 219 if (match) { 220 int offset = START_SKIP[i].length(); 221 for (int m = position + offset; m < statement.length; m++) { 222 if (statement[m] == STOP_SKIP[i].charAt(0)) { 223 boolean endMatch = true; 224 int endPos = m; 225 for (int n = 1; n < STOP_SKIP[i].length(); n++) { 226 if (m + n >= statement.length) { 227 // last comment not closed properly 228 return statement.length; 229 } 230 if (statement[m + n] != STOP_SKIP[i].charAt(n)) { 231 endMatch = false; 232 break; 233 } 234 endPos = m + n; 235 } 236 if (endMatch) { 237 // found character sequence ending comment or quote 238 return endPos + 1; 239 } 240 } 241 } 242 // character sequence ending comment or quote not found 243 return statement.length; 244 } 245 } 246 } 247 return position; 248 } 249 250 /** 251 * Parse the SQL statement and locate any placeholders or named parameters. Named 252 * parameters are substituted for a JDBC placeholder, and any select list is expanded 253 * to the required number of placeholders. Select lists may contain an array of 254 * objects, and in that case the placeholders will be grouped and enclosed with 255 * parentheses. This allows for the use of "expression lists" in the SQL statement 256 * like: <br /><br /> 257 * {@code select id, name, state from table where (name, age) in (('John', 35), ('Ann', 50))} 258 * <p>The parameter values passed in are used to determine the number of placeholders to 259 * be used for a select list. Select lists should be limited to 100 or fewer elements. 260 * A larger number of elements is not guaranteed to be supported by the database and 261 * is strictly vendor-dependent. 262 * @param parsedSql the parsed representation of the SQL statement 263 * @param paramSource the source for named parameters 264 * @return the SQL statement with substituted parameters 265 * @see #parseSqlStatement 266 */ 267 public static String substituteNamedParameters(ParsedSql parsedSql, @Nullable SqlParameterSource paramSource) { 268 String originalSql = parsedSql.getOriginalSql(); 269 List<String> paramNames = parsedSql.getParameterNames(); 270 if (paramNames.isEmpty()) { 271 return originalSql; 272 } 273 StringBuilder actualSql = new StringBuilder(originalSql.length()); 274 int lastIndex = 0; 275 for (int i = 0; i < paramNames.size(); i++) { 276 String paramName = paramNames.get(i); 277 int[] indexes = parsedSql.getParameterIndexes(i); 278 int startIndex = indexes[0]; 279 int endIndex = indexes[1]; 280 actualSql.append(originalSql, lastIndex, startIndex); 281 if (paramSource != null && paramSource.hasValue(paramName)) { 282 Object value = paramSource.getValue(paramName); 283 if (value instanceof SqlParameterValue) { 284 value = ((SqlParameterValue) value).getValue(); 285 } 286 if (value instanceof Iterable) { 287 Iterator<?> entryIter = ((Iterable<?>) value).iterator(); 288 int k = 0; 289 while (entryIter.hasNext()) { 290 if (k > 0) { 291 actualSql.append(", "); 292 } 293 k++; 294 Object entryItem = entryIter.next(); 295 if (entryItem instanceof Object[]) { 296 Object[] expressionList = (Object[]) entryItem; 297 actualSql.append('('); 298 for (int m = 0; m < expressionList.length; m++) { 299 if (m > 0) { 300 actualSql.append(", "); 301 } 302 actualSql.append('?'); 303 } 304 actualSql.append(')'); 305 } 306 else { 307 actualSql.append('?'); 308 } 309 } 310 } 311 else { 312 actualSql.append('?'); 313 } 314 } 315 else { 316 actualSql.append('?'); 317 } 318 lastIndex = endIndex; 319 } 320 actualSql.append(originalSql, lastIndex, originalSql.length()); 321 return actualSql.toString(); 322 } 323 324 /** 325 * Convert a Map of named parameter values to a corresponding array. 326 * @param parsedSql the parsed SQL statement 327 * @param paramSource the source for named parameters 328 * @param declaredParams the List of declared SqlParameter objects 329 * (may be {@code null}). If specified, the parameter metadata will 330 * be built into the value array in the form of SqlParameterValue objects. 331 * @return the array of values 332 */ 333 public static Object[] buildValueArray( 334 ParsedSql parsedSql, SqlParameterSource paramSource, @Nullable List<SqlParameter> declaredParams) { 335 336 Object[] paramArray = new Object[parsedSql.getTotalParameterCount()]; 337 if (parsedSql.getNamedParameterCount() > 0 && parsedSql.getUnnamedParameterCount() > 0) { 338 throw new InvalidDataAccessApiUsageException( 339 "Not allowed to mix named and traditional ? placeholders. You have " + 340 parsedSql.getNamedParameterCount() + " named parameter(s) and " + 341 parsedSql.getUnnamedParameterCount() + " traditional placeholder(s) in statement: " + 342 parsedSql.getOriginalSql()); 343 } 344 List<String> paramNames = parsedSql.getParameterNames(); 345 for (int i = 0; i < paramNames.size(); i++) { 346 String paramName = paramNames.get(i); 347 try { 348 SqlParameter param = findParameter(declaredParams, paramName, i); 349 paramArray[i] = (param != null ? new SqlParameterValue(param, paramSource.getValue(paramName)) : 350 SqlParameterSourceUtils.getTypedValue(paramSource, paramName)); 351 } 352 catch (IllegalArgumentException ex) { 353 throw new InvalidDataAccessApiUsageException( 354 "No value supplied for the SQL parameter '" + paramName + "': " + ex.getMessage()); 355 } 356 } 357 return paramArray; 358 } 359 360 /** 361 * Find a matching parameter in the given list of declared parameters. 362 * @param declaredParams the declared SqlParameter objects 363 * @param paramName the name of the desired parameter 364 * @param paramIndex the index of the desired parameter 365 * @return the declared SqlParameter, or {@code null} if none found 366 */ 367 @Nullable 368 private static SqlParameter findParameter( 369 @Nullable List<SqlParameter> declaredParams, String paramName, int paramIndex) { 370 371 if (declaredParams != null) { 372 // First pass: Look for named parameter match. 373 for (SqlParameter declaredParam : declaredParams) { 374 if (paramName.equals(declaredParam.getName())) { 375 return declaredParam; 376 } 377 } 378 // Second pass: Look for parameter index match. 379 if (paramIndex < declaredParams.size()) { 380 SqlParameter declaredParam = declaredParams.get(paramIndex); 381 // Only accept unnamed parameters for index matches. 382 if (declaredParam.getName() == null) { 383 return declaredParam; 384 } 385 } 386 } 387 return null; 388 } 389 390 /** 391 * Determine whether a parameter name ends at the current position, 392 * that is, whether the given character qualifies as a separator. 393 */ 394 private static boolean isParameterSeparator(char c) { 395 return (c < 128 && separatorIndex[c]) || Character.isWhitespace(c); 396 } 397 398 /** 399 * Convert parameter types from an SqlParameterSource into a corresponding int array. 400 * This is necessary in order to reuse existing methods on JdbcTemplate. 401 * Any named parameter types are placed in the correct position in the 402 * Object array based on the parsed SQL statement info. 403 * @param parsedSql the parsed SQL statement 404 * @param paramSource the source for named parameters 405 */ 406 public static int[] buildSqlTypeArray(ParsedSql parsedSql, SqlParameterSource paramSource) { 407 int[] sqlTypes = new int[parsedSql.getTotalParameterCount()]; 408 List<String> paramNames = parsedSql.getParameterNames(); 409 for (int i = 0; i < paramNames.size(); i++) { 410 String paramName = paramNames.get(i); 411 sqlTypes[i] = paramSource.getSqlType(paramName); 412 } 413 return sqlTypes; 414 } 415 416 /** 417 * Convert parameter declarations from an SqlParameterSource to a corresponding List of SqlParameters. 418 * This is necessary in order to reuse existing methods on JdbcTemplate. 419 * The SqlParameter for a named parameter is placed in the correct position in the 420 * resulting list based on the parsed SQL statement info. 421 * @param parsedSql the parsed SQL statement 422 * @param paramSource the source for named parameters 423 */ 424 public static List<SqlParameter> buildSqlParameterList(ParsedSql parsedSql, SqlParameterSource paramSource) { 425 List<String> paramNames = parsedSql.getParameterNames(); 426 List<SqlParameter> params = new ArrayList<>(paramNames.size()); 427 for (String paramName : paramNames) { 428 params.add(new SqlParameter( 429 paramName, paramSource.getSqlType(paramName), paramSource.getTypeName(paramName))); 430 } 431 return params; 432 } 433 434 435 //------------------------------------------------------------------------- 436 // Convenience methods operating on a plain SQL String 437 //------------------------------------------------------------------------- 438 439 /** 440 * Parse the SQL statement and locate any placeholders or named parameters. 441 * Named parameters are substituted for a JDBC placeholder. 442 * <p>This is a shortcut version of 443 * {@link #parseSqlStatement(String)} in combination with 444 * {@link #substituteNamedParameters(ParsedSql, SqlParameterSource)}. 445 * @param sql the SQL statement 446 * @return the actual (parsed) SQL statement 447 */ 448 public static String parseSqlStatementIntoString(String sql) { 449 ParsedSql parsedSql = parseSqlStatement(sql); 450 return substituteNamedParameters(parsedSql, null); 451 } 452 453 /** 454 * Parse the SQL statement and locate any placeholders or named parameters. 455 * Named parameters are substituted for a JDBC placeholder and any select list 456 * is expanded to the required number of placeholders. 457 * <p>This is a shortcut version of 458 * {@link #substituteNamedParameters(ParsedSql, SqlParameterSource)}. 459 * @param sql the SQL statement 460 * @param paramSource the source for named parameters 461 * @return the SQL statement with substituted parameters 462 */ 463 public static String substituteNamedParameters(String sql, SqlParameterSource paramSource) { 464 ParsedSql parsedSql = parseSqlStatement(sql); 465 return substituteNamedParameters(parsedSql, paramSource); 466 } 467 468 /** 469 * Convert a Map of named parameter values to a corresponding array. 470 * <p>This is a shortcut version of 471 * {@link #buildValueArray(ParsedSql, SqlParameterSource, java.util.List)}. 472 * @param sql the SQL statement 473 * @param paramMap the Map of parameters 474 * @return the array of values 475 */ 476 public static Object[] buildValueArray(String sql, Map<String, ?> paramMap) { 477 ParsedSql parsedSql = parseSqlStatement(sql); 478 return buildValueArray(parsedSql, new MapSqlParameterSource(paramMap), null); 479 } 480 481 482 private static class ParameterHolder { 483 484 private final String parameterName; 485 486 private final int startIndex; 487 488 private final int endIndex; 489 490 public ParameterHolder(String parameterName, int startIndex, int endIndex) { 491 this.parameterName = parameterName; 492 this.startIndex = startIndex; 493 this.endIndex = endIndex; 494 } 495 496 public String getParameterName() { 497 return this.parameterName; 498 } 499 500 public int getStartIndex() { 501 return this.startIndex; 502 } 503 504 public int getEndIndex() { 505 return this.endIndex; 506 } 507 } 508 509}