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}