001package io.ebeaninternal.api; 002 003import io.ebeaninternal.server.persist.MultiValueWrapper; 004import io.ebeaninternal.server.querydefn.NaturalKeyBindParam; 005 006import java.io.Serializable; 007import java.util.ArrayList; 008import java.util.Collection; 009import java.util.LinkedHashMap; 010import java.util.List; 011import java.util.Map; 012import java.util.Map.Entry; 013 014/** 015 * Parameters used for binding to a statement. 016 * <p> 017 * Supports ordered or named parameters. 018 * </p> 019 */ 020public class BindParams implements Serializable { 021 022 private static final long serialVersionUID = 4541081933302086285L; 023 024 private final List<Param> positionedParameters = new ArrayList<>(); 025 026 private final Map<String, Param> namedParameters = new LinkedHashMap<>(); 027 028 /** 029 * This is the sql. For named parameters this is the sql after the named 030 * parameters have been replaced with question mark place holders and the 031 * parameters have been ordered by addNamedParamInOrder(). 032 */ 033 private String preparedSql; 034 035 /** 036 * Bind hash and count used to detect when the bind values have changed such 037 * that the generated SQL (with named parameters) needs to be recalculated. 038 */ 039 private String bindHash; 040 041 /** 042 * Helper to add positioned parameters in order. 043 */ 044 private int addPos; 045 046 public BindParams() { 047 } 048 049 /** 050 * Reset positioned parameters (usually due to bind parameter expansion). 051 */ 052 public void reset() { 053 bindHash = null; 054 positionedParameters.clear(); 055 } 056 057 public int queryBindHash() { 058 int hc = namedParameters.hashCode(); 059 for (Param positionedParameter : positionedParameters) { 060 hc = hc * 92821 + positionedParameter.hashCode(); 061 } 062 return hc; 063 } 064 065 /** 066 * Return the hash that should be included with the query plan. 067 * <p> 068 * This is to handle binding collections to in clauses. The number of values 069 * in the collection effects the query (number of bind values) and so must be 070 * taken into account when calculating the query hash. 071 * </p> 072 */ 073 public String calcQueryPlanHash() { 074 StringBuilder builder = new StringBuilder(); 075 buildQueryPlanHash(builder); 076 return builder.toString(); 077 } 078 079 /** 080 * Calculate and return a query plan bind hash with total bind count. 081 */ 082 public void buildQueryPlanHash(StringBuilder builder) { 083 int tempBindCount; 084 int bc = 0; 085 for (Param param : positionedParameters) { 086 tempBindCount = param.queryBindCount(); 087 bc += tempBindCount; 088 builder.append("p").append(bc).append(" ?:").append(tempBindCount).append(","); 089 } 090 091 for (Map.Entry<String, Param> entry : namedParameters.entrySet()) { 092 tempBindCount = entry.getValue().queryBindCount(); 093 bc += tempBindCount; 094 builder.append("n").append(bc).append(" k:").append(entry.getKey()).append(" ?:").append(tempBindCount).append(","); 095 } 096 } 097 098 /** 099 * Return a deep copy of the BindParams. 100 */ 101 public BindParams copy() { 102 BindParams copy = new BindParams(); 103 for (Param p : positionedParameters) { 104 copy.positionedParameters.add(p.copy()); 105 } 106 for (Entry<String, Param> entry : namedParameters.entrySet()) { 107 copy.namedParameters.put(entry.getKey(), entry.getValue().copy()); 108 } 109 return copy; 110 } 111 112 /** 113 * Return true if there are no bind parameters. 114 */ 115 public boolean isEmpty() { 116 return positionedParameters.isEmpty() && namedParameters.isEmpty(); 117 } 118 119 /** 120 * Return a Natural Key bind param if supported. 121 */ 122 public NaturalKeyBindParam getNaturalKeyBindParam() { 123 if (!positionedParameters.isEmpty()) { 124 return null; 125 } 126 if (namedParameters.size() == 1) { 127 Entry<String, Param> e = namedParameters.entrySet().iterator().next(); 128 return new NaturalKeyBindParam(e.getKey(), e.getValue().getInValue()); 129 } 130 return null; 131 } 132 133 public int size() { 134 return positionedParameters.size(); 135 } 136 137 /** 138 * Return true if named parameters are being used and they have not yet been 139 * ordered. The sql needs to be prepared (named replaced with ?) and the 140 * parameters ordered. 141 */ 142 public boolean requiresNamedParamsPrepare() { 143 return !namedParameters.isEmpty(); 144 } 145 146 /** 147 * Set a null parameter using position. 148 */ 149 public void setNullParameter(int position, int jdbcType) { 150 Param p = getParam(position); 151 p.setInNullType(jdbcType); 152 } 153 154 /** 155 * Set an In Out parameter using position. 156 */ 157 public void setParameter(int position, Object value, int outType) { 158 159 Param p = getParam(position); 160 p.setInValue(value); 161 p.setOutType(outType); 162 } 163 164 public void setNextParameters(Object... values) { 165 for (Object value : values) { 166 setNextParameter(value); 167 } 168 } 169 170 /** 171 * Bind the next positioned parameter. 172 */ 173 public void setNextParameter(Object value) { 174 setParameter(++addPos, value); 175 } 176 177 /** 178 * Using position set the In value of a parameter. Note that for nulls you 179 * must use setNullParameter. 180 */ 181 public void setParameter(int position, Object value) { 182 183 Param p = getParam(position); 184 if (value instanceof Collection) { 185 // use of postgres ANY with positioned parameter 186 value = new MultiValueWrapper((Collection)value); 187 } 188 p.setInValue(value); 189 } 190 191 /** 192 * Register the parameter as an Out parameter using position. 193 */ 194 public void registerOut(int position, int outType) { 195 Param p = getParam(position); 196 p.setOutType(outType); 197 } 198 199 private Param getParam(String name) { 200 return namedParameters.computeIfAbsent(name, k -> new Param()); 201 } 202 203 private Param getParam(int position) { 204 int more = position - positionedParameters.size(); 205 if (more > 0) { 206 for (int i = 0; i < more; i++) { 207 positionedParameters.add(new Param()); 208 } 209 } 210 return positionedParameters.get(position - 1); 211 } 212 213 /** 214 * Set a named In Out parameter. 215 */ 216 public void setParameter(String name, Object value, int outType) { 217 218 Param p = getParam(name); 219 p.setInValue(value); 220 p.setOutType(outType); 221 } 222 223 /** 224 * Set a named In parameter that is null. 225 */ 226 public void setNullParameter(String name, int jdbcType) { 227 Param p = getParam(name); 228 p.setInNullType(jdbcType); 229 } 230 231 /** 232 * Set a named In parameter that is not null. 233 */ 234 public Param setParameter(String name, Object value) { 235 236 Param p = getParam(name); 237 p.setInValue(value); 238 return p; 239 } 240 241 /** 242 * Set an encryption key as a bind value. 243 * <p> 244 * Needs special treatment as the value should not be included in a log. 245 * </p> 246 */ 247 public Param setEncryptionKey(String name, Object value) { 248 Param p = getParam(name); 249 p.setEncryptionKey(value); 250 return p; 251 } 252 253 /** 254 * Register the named parameter as an Out parameter. 255 */ 256 public void registerOut(String name, int outType) { 257 Param p = getParam(name); 258 p.setOutType(outType); 259 } 260 261 /** 262 * Return the Parameter for a given position. 263 */ 264 public Param getParameter(int position) { 265 // Used to read Out value by CallableSql 266 return getParam(position); 267 } 268 269 /** 270 * Return the named parameter. 271 */ 272 public Param getParameter(String name) { 273 return getParam(name); 274 } 275 276 /** 277 * Return the values of ordered parameters. 278 */ 279 public List<Param> positionedParameters() { 280 return positionedParameters; 281 } 282 283 /** 284 * Set the sql with named parameters replaced with place holder ?. 285 */ 286 public void setPreparedSql(String preparedSql) { 287 this.preparedSql = preparedSql; 288 } 289 290 /** 291 * Return the sql with ? place holders (named parameters have been processed 292 * and ordered). 293 */ 294 public String getPreparedSql() { 295 return preparedSql; 296 } 297 298 /** 299 * Return true if the bind hash and count has not changed. 300 */ 301 public boolean isSameBindHash() { 302 303 if (bindHash == null) { 304 bindHash = calcQueryPlanHash(); 305 return false; 306 } 307 String oldPlan = bindHash; 308 bindHash = calcQueryPlanHash(); 309 return bindHash.equals(oldPlan); 310 } 311 312 /** 313 * Create a new positioned parameters orderedList. 314 */ 315 public OrderedList createOrderedList() { 316 positionedParameters.clear(); 317 return new OrderedList(positionedParameters); 318 } 319 320 /** 321 * The bind parameters in the correct binding order. 322 * <p> 323 * This is the result of converting sql with named parameters 324 * into sql with ? and ordered parameters. 325 * </p> 326 */ 327 public static final class OrderedList { 328 329 private final List<Param> paramList; 330 331 private final StringBuilder preparedSql; 332 333 public OrderedList() { 334 this(new ArrayList<>()); 335 } 336 337 public OrderedList(List<Param> paramList) { 338 this.paramList = paramList; 339 this.preparedSql = new StringBuilder(); 340 } 341 342 /** 343 * Add a parameter in the correct binding order. 344 */ 345 public void add(Param param) { 346 paramList.add(param); 347 } 348 349 /** 350 * Return the number of bind parameters in this list. 351 */ 352 public int size() { 353 return paramList.size(); 354 } 355 356 /** 357 * Returns the ordered list of bind parameters. 358 */ 359 public List<Param> list() { 360 return paramList; 361 } 362 363 /** 364 * Append parsedSql that has named parameters converted into ?. 365 */ 366 public void appendSql(String parsedSql) { 367 preparedSql.append(parsedSql); 368 } 369 370 public String getPreparedSql() { 371 return preparedSql.toString(); 372 } 373 } 374 375 /** 376 * A In Out capable parameter for the CallableStatement. 377 */ 378 public static final class Param implements Serializable { 379 380 private static final long serialVersionUID = 1L; 381 382 private boolean encryptionKey; 383 384 private boolean isInParam; 385 386 private boolean isOutParam; 387 388 private int type; 389 390 private Object inValue; 391 392 private Object outValue; 393 394 /** 395 * Construct a Parameter. 396 */ 397 public Param() { 398 } 399 400 public int queryBindCount() { 401 if (inValue == null) { 402 return 0; 403 } 404 if (inValue instanceof Collection<?>) { 405 return ((Collection<?>) inValue).size(); 406 } 407 return 1; 408 } 409 410 /** 411 * Create a deep copy of the Param. 412 */ 413 public Param copy() { 414 Param copy = new Param(); 415 copy.isInParam = isInParam; 416 copy.isOutParam = isOutParam; 417 copy.type = type; 418 copy.inValue = inValue; 419 copy.outValue = outValue; 420 return copy; 421 } 422 423 @Override 424 public int hashCode() { 425 int hc = getClass().hashCode(); 426 hc = hc * 92821 + (isInParam ? 0 : 1); 427 hc = hc * 92821 + (isOutParam ? 0 : 1); 428 hc = hc * 92821 + (type); 429 hc = hc * 92821 + (inValue == null ? 0 : inValue.hashCode()); 430 return hc; 431 } 432 433 @Override 434 public boolean equals(Object o) { 435 return o != null && (o == this || (o instanceof Param) && hashCode() == o.hashCode()); 436 } 437 438 /** 439 * Return true if this is an In parameter that needs to be bound before 440 * execution. 441 */ 442 public boolean isInParam() { 443 return isInParam; 444 } 445 446 /** 447 * Return true if this is an out parameter that needs to be registered 448 * before execution. 449 */ 450 public boolean isOutParam() { 451 return isOutParam; 452 } 453 454 /** 455 * Return the jdbc type of this parameter. Used for registering Out 456 * parameters and setting NULL In parameters. 457 */ 458 public int getType() { 459 return type; 460 } 461 462 /** 463 * Set the Out parameter type. 464 */ 465 public void setOutType(int type) { 466 this.type = type; 467 this.isOutParam = true; 468 } 469 470 /** 471 * Set the In value. 472 */ 473 public void setInValue(Object in) { 474 this.inValue = in; 475 this.isInParam = true; 476 } 477 478 /** 479 * Set an encryption key (which can not be logged). 480 */ 481 public void setEncryptionKey(Object in) { 482 this.inValue = in; 483 this.isInParam = true; 484 this.encryptionKey = true; 485 } 486 487 /** 488 * Specify that the In parameter is NULL and the specific type that it 489 * is. 490 */ 491 public void setInNullType(int type) { 492 this.type = type; 493 this.inValue = null; 494 this.isInParam = true; 495 } 496 497 /** 498 * Return the OUT value that was retrieved. This value is set after 499 * CallableStatement was executed. 500 */ 501 public Object getOutValue() { 502 return outValue; 503 } 504 505 /** 506 * Return the In value. If this is null, then the type should be used to 507 * specify the type of the null. 508 */ 509 public Object getInValue() { 510 return inValue; 511 } 512 513 /** 514 * Set the OUT value returned by a CallableStatement after it has 515 * executed. 516 */ 517 public void setOutValue(Object out) { 518 this.outValue = out; 519 } 520 521 /** 522 * If true do not include this value in a transaction log. 523 */ 524 public boolean isEncryptionKey() { 525 return encryptionKey; 526 } 527 528 } 529}