1 /*
2  * Copyright 2013-2020 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.data.jpa.repository.query;
17
18 import static java.util.regex.Pattern.*;
19 import static org.springframework.util.ObjectUtils.*;
20
21 import java.lang.reflect.Array;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.List;
26 import java.util.function.BiFunction;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29
30 import org.springframework.data.repository.query.SpelQueryContext;
31 import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
32 import org.springframework.data.repository.query.parser.Part.Type;
33 import org.springframework.lang.Nullable;
34 import org.springframework.util.Assert;
35 import org.springframework.util.ObjectUtils;
36 import org.springframework.util.StringUtils;
37
38 /**
39  * Encapsulation of a JPA query String. Offers access to parameters as bindings. The internal query String is cleaned
40  * from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations
41  * in the {@link ParameterBinding#prepare(Object)} method. Note that this class also handles replacing SpEL expressions
42  * with synthetic bind parameters
43  *
44  * @author Oliver Gierke
45  * @author Thomas Darimont
46  * @author Oliver Wehrens
47  * @author Mark Paluch
48  * @author Jens Schauder
49  */

50 class StringQuery implements DeclaredQuery {
51
52     private final String query;
53     private final List<ParameterBinding> bindings;
54     private final @Nullable String alias;
55     private final boolean hasConstructorExpression;
56     private final boolean containsPageableInSpel;
57     private final boolean usesJdbcStyleParameters;
58
59     /**
60      * Creates a new {@link StringQuery} from the given JPQL query.
61      *
62      * @param query must not be {@literal null} or empty.
63      */

64     @SuppressWarnings("deprecation")
65     StringQuery(String query) {
66
67         Assert.hasText(query, "Query must not be null or empty!");
68
69         this.bindings = new ArrayList<>();
70         this.containsPageableInSpel = query.contains("#pageable");
71
72         Metadata queryMeta = new Metadata();
73         this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
74                 this.bindings, queryMeta);
75
76         this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
77         this.alias = QueryUtils.detectAlias(query);
78         this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query);
79     }
80
81     /**
82      * Returns whether we have found some like bindings.
83      */

84     boolean hasParameterBindings() {
85         return !bindings.isEmpty();
86     }
87
88     String getProjection() {
89         return QueryUtils.getProjection(query);
90     }
91
92     /*
93      * (non-Javadoc)
94      * @see org.springframework.data.jpa.repository.query.DeclaredQuery#getParameterBindings()
95      */

96     @Override
97     public List<ParameterBinding> getParameterBindings() {
98         return bindings;
99     }
100
101     /*
102      * (non-Javadoc)
103      * @see org.springframework.data.jpa.repository.query.DeclaredQuery#deriveCountQuery(java.lang.String, java.lang.String)
104      */

105     @Override
106     @SuppressWarnings("deprecation")
107     public DeclaredQuery deriveCountQuery(@Nullable String countQuery, @Nullable String countQueryProjection) {
108
109         return DeclaredQuery
110                 .of(countQuery != null ? countQuery : QueryUtils.createCountQueryFor(query, countQueryProjection));
111     }
112
113     /*
114      * (non-Javadoc)
115      * @see org.springframework.data.jpa.repository.query.DeclaredQuery#usesJdbcStyleParameters()
116      */

117     @Override
118     public boolean usesJdbcStyleParameters() {
119         return usesJdbcStyleParameters;
120     }
121
122     /*
123      * (non-Javadoc)
124      * @see org.springframework.data.jpa.repository.query.DeclaredQuery#getQueryString()
125      */

126     @Override
127     public String getQueryString() {
128         return query;
129     }
130
131     /*
132      * (non-Javadoc)
133      * @see org.springframework.data.jpa.repository.query.DeclaredQuery#getAlias()
134      */

135     @Override
136     @Nullable
137     public String getAlias() {
138         return alias;
139     }
140
141     /*
142      * (non-Javadoc)
143      * @see org.springframework.data.jpa.repository.query.DeclaredQuery#hasConstructorExpression()
144      */

145     @Override
146     public boolean hasConstructorExpression() {
147         return hasConstructorExpression;
148     }
149
150     /*
151      * (non-Javadoc)
152      * @see org.springframework.data.jpa.repository.query.DeclaredQuery#isDefaultProjection()
153      */

154     @Override
155     public boolean isDefaultProjection() {
156         return getProjection().equalsIgnoreCase(alias);
157     }
158
159     /*
160      * (non-Javadoc)
161      * @see org.springframework.data.jpa.repository.query.DeclaredQuery#hasNamedParameter()
162      */

163     @Override
164     public boolean hasNamedParameter() {
165         return bindings.stream().anyMatch(b -> b.getName() != null);
166     }
167
168     /*
169      * (non-Javadoc)
170      * @see org.springframework.data.jpa.repository.query.DeclaredQuery#usesPaging()
171      */

172     @Override
173     public boolean usesPaging() {
174         return containsPageableInSpel;
175     }
176
177     /**
178      * A parser that extracts the parameter bindings from a given query string.
179      *
180      * @author Thomas Darimont
181      */

182     enum ParameterBindingParser {
183
184         INSTANCE;
185
186         private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
187         public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))";
188         // .....................................................................^ not followed by a hash or a letter.
189         // .................................................................^ zero or more digits.
190         // .............................................................^ start with a question mark.
191         private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER);
192         private static final Pattern PARAMETER_BINDING_PATTERN;
193         private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type! "
194                 + "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same binding.";
195         private static final int INDEXED_PARAMETER_GROUP = 4;
196         private static final int NAMED_PARAMETER_GROUP = 6;
197         private static final int COMPARISION_TYPE_GROUP = 1;
198
199         static {
200
201             List<String> keywords = new ArrayList<>();
202
203             for (ParameterBindingType type : ParameterBindingType.values()) {
204                 if (type.getKeyword() != null) {
205                     keywords.add(type.getKeyword());
206                 }
207             }
208
209             StringBuilder builder = new StringBuilder();
210             builder.append("(");
211             builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords
212             builder.append(")?");
213             builder.append("(?: )?"); // some whitespace
214             builder.append("\\(?"); // optional braces around parameters
215             builder.append("(");
216             builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index
217             builder.append("|"); // or
218
219             // named parameter and the parameter name
220             builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?");
221
222             builder.append(")");
223             builder.append("\\)?"); // optional braces around parameters
224
225             PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
226         }
227
228         /**
229          * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns
230          * the cleaned up query.
231          */

232         private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
233                 List<ParameterBinding> bindings, Metadata queryMeta) {
234
235             int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);
236             boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1;
237
238             /*
239              * Prefer indexed access over named parameters if only SpEL Expression parameters are present.
240              */

241             if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) {
242                 parametersShouldBeAccessedByIndex = true;
243                 greatestParameterIndex = 0;
244             }
245
246             SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIndex,
247                     greatestParameterIndex);
248
249             String resultingQuery = spelExtractor.getQueryString();
250             Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery);
251
252             int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
253
254             boolean usesJpaStyleParameters = false;
255             while (matcher.find()) {
256
257                 if (spelExtractor.isQuoted(matcher.start())) {
258                     continue;
259                 }
260
261                 String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP);
262                 String parameterName = parameterIndexString != null ? null : matcher.group(NAMED_PARAMETER_GROUP);
263                 Integer parameterIndex = getParameterIndex(parameterIndexString);
264
265                 String typeSource = matcher.group(COMPARISION_TYPE_GROUP);
266                 String expression = spelExtractor.getParameter(parameterName == null ? parameterIndexString : parameterName);
267                 String replacement = null;
268
269                 Assert.isTrue(parameterIndexString != null || parameterName != null, () -> String.format("We need either a name or an index! Offending query string: %s", query));
270
271                 expressionParameterIndex++;
272                 if ("".equals(parameterIndexString)) {
273
274                     queryMeta.usesJdbcStyleParameters = true;
275                     parameterIndex = expressionParameterIndex;
276                 } else {
277                     usesJpaStyleParameters = true;
278                 }
279
280                 if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) {
281                     throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported!");
282                 }
283
284                 switch (ParameterBindingType.of(typeSource)) {
285
286                     case LIKE:
287
288                         Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
289                         replacement = matcher.group(3);
290
291                         if (parameterIndex != null) {
292                             checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
293                         } else {
294                             checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings);
295
296                             replacement = expression != null ? ":" + parameterName : matcher.group(5);
297                         }
298
299                         break;
300
301                     case IN:
302
303                         if (parameterIndex != null) {
304                             checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings);
305                         } else {
306                             checkAndRegister(new InParameterBinding(parameterName, expression), bindings);
307                         }
308
309                         break;
310
311                     case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
312                     default:
313
314                         bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression)
315                                 : new ParameterBinding(parameterName, null, expression));
316                 }
317
318                 if (replacement != null) {
319                     resultingQuery = replaceFirst(resultingQuery, matcher.group(2), replacement);
320                 }
321
322             }
323
324             return resultingQuery;
325         }
326
327         private static SpelExtractor createSpelExtractor(String queryWithSpel, boolean parametersShouldBeAccessedByIndex,
328                 int greatestParameterIndex) {
329
330             /*
331              * If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to
332              * not mix-up with the actual parameter indices.
333              */

334             int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
335
336             BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
337                     ? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1)
338                     : (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1);
339
340             String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
341
342             BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name;
343
344             return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
345         }
346
347         private static String replaceFirst(String text, String substring, String replacement) {
348
349             int index = text.indexOf(substring);
350             if (index < 0) {
351                 return text;
352             }
353
354             return text.substring(0, index) + replacement + text.substring(index + substring.length());
355         }
356
357         @Nullable
358         private static Integer getParameterIndex(@Nullable String parameterIndexString) {
359
360             if (parameterIndexString == null || parameterIndexString.isEmpty()) {
361                 return null;
362             }
363             return Integer.valueOf(parameterIndexString);
364         }
365
366         private static int tryFindGreatestParameterIndexIn(String query) {
367
368             Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
369
370             int greatestParameterIndex = -1;
371             while (parameterIndexMatcher.find()) {
372
373                 String parameterIndexString = parameterIndexMatcher.group(1);
374                 Integer parameterIndex = getParameterIndex(parameterIndexString);
375                 if (parameterIndex != null) {
376                     greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex);
377                 }
378             }
379
380             return greatestParameterIndex;
381         }
382
383         private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {
384
385             bindings.stream() //
386                     .filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition())) //
387                     .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding)));
388
389             if (!bindings.contains(binding)) {
390                 bindings.add(binding);
391             }
392         }
393
394         /**
395          * An enum for the different types of bindings.
396          *
397          * @author Thomas Darimont
398          * @author Oliver Gierke
399          */

400         private enum ParameterBindingType {
401
402             // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace
403             // character, while = does not.
404             LIKE("like "), IN("in "), AS_IS(null);
405
406             private final @Nullable String keyword;
407
408             ParameterBindingType(@Nullable String keyword) {
409                 this.keyword = keyword;
410             }
411
412             /**
413              * Returns the keyword that will trigger the binding type or {@literal nullif the type is not triggered by a
414              * keyword.
415              *
416              * @return the keyword
417              */

418             @Nullable
419             public String getKeyword() {
420                 return keyword;
421             }
422
423             /**
424              * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal #AS_IS} in
425              * case no other {@link ParameterBindingType} could be found.
426              */

427             static ParameterBindingType of(String typeSource) {
428
429                 if (!StringUtils.hasText(typeSource)) {
430                     return AS_IS;
431                 }
432
433                 for (ParameterBindingType type : values()) {
434                     if (type.name().equalsIgnoreCase(typeSource.trim())) {
435                         return type;
436                     }
437                 }
438
439                 throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s!", typeSource));
440             }
441         }
442     }
443
444     /**
445      * A generic parameter binding with name or position information.
446      *
447      * @author Thomas Darimont
448      */

449     static class ParameterBinding {
450
451         private final @Nullable String name;
452         private final @Nullable String expression;
453         private final @Nullable Integer position;
454
455         /**
456          * Creates a new {@link ParameterBinding} for the parameter with the given position.
457          *
458          * @param position must not be {@literal null}.
459          */

460         ParameterBinding(Integer position) {
461             this(null, position, null);
462         }
463
464         /**
465          * Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression
466          * information. Either {@literal name} or {@literal position} must be not {@literal null}.
467          *
468          * @param name of the parameter may be {@literal null}.
469          * @param position of the parameter may be {@literal null}.
470          * @param expression the expression to apply to any value for this parameter.
471          */

472         ParameterBinding(@Nullable String name, @Nullable Integer position, @Nullable String expression) {
473
474             if (name == null) {
475                 Assert.notNull(position, "Position must not be null!");
476             }
477
478             if (position == null) {
479                 Assert.notNull(name, "Name must not be null!");
480             }
481
482             this.name = name;
483             this.position = position;
484             this.expression = expression;
485         }
486
487         /**
488          * Returns whether the binding has the given name. Will always be {@literal false} in case the
489          * {@link ParameterBinding} has been set up from a position.
490          */

491         boolean hasName(@Nullable String name) {
492             return this.position == null && this.name != null && this.name.equals(name);
493         }
494
495         /**
496          * Returns whether the binding has the given position. Will always be {@literal false} in case the
497          * {@link ParameterBinding} has been set up from a name.
498          */

499         boolean hasPosition(@Nullable Integer position) {
500             return position != null && this.name == null && position.equals(this.position);
501         }
502
503         /**
504          * @return the name
505          */

506         @Nullable
507         public String getName() {
508             return name;
509         }
510
511         /**
512          * @return the name
513          * @throws IllegalStateException if the name is not available.
514          * @since 2.0
515          */

516         String getRequiredName() throws IllegalStateException {
517
518             String name = getName();
519
520             if (name != null) {
521                 return name;
522             }
523
524             throw new IllegalStateException(String.format("Required name for %s not available!"this));
525         }
526
527         /**
528          * @return the position
529          */

530         @Nullable
531         Integer getPosition() {
532             return position;
533         }
534
535         /**
536          * @return the position
537          * @throws IllegalStateException if the position is not available.
538          * @since 2.0
539          */

540         int getRequiredPosition() throws IllegalStateException {
541
542             Integer position = getPosition();
543
544             if (position != null) {
545                 return position;
546             }
547
548             throw new IllegalStateException(String.format("Required position for %s not available!"this));
549         }
550
551         /**
552          * @return {@literal trueif this parameter binding is a synthetic SpEL expression.
553          */

554         public boolean isExpression() {
555             return this.expression != null;
556         }
557
558         /*
559          * (non-Javadoc)
560          * @see java.lang.Object#hashCode()
561          */

562         @Override
563         public int hashCode() {
564
565             int result = 17;
566
567             result += nullSafeHashCode(this.name);
568             result += nullSafeHashCode(this.position);
569             result += nullSafeHashCode(this.expression);
570
571             return result;
572         }
573
574         /*
575          * (non-Javadoc)
576          * @see java.lang.Object#equals(java.lang.Object)
577          */

578         @Override
579         public boolean equals(Object obj) {
580
581             if (!(obj instanceof ParameterBinding)) {
582                 return false;
583             }
584
585             ParameterBinding that = (ParameterBinding) obj;
586
587             return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position)
588                     && nullSafeEquals(this.expression, that.expression);
589         }
590
591         /*
592          * (non-Javadoc)
593          * @see java.lang.Object#toString()
594          */

595         @Override
596         public String toString() {
597             return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(),
598                     getExpression());
599         }
600
601         /**
602          * @param valueToBind value to prepare
603          */

604         @Nullable
605         public Object prepare(@Nullable Object valueToBind) {
606             return valueToBind;
607         }
608
609         @Nullable
610         public String getExpression() {
611             return expression;
612         }
613     }
614
615     /**
616      * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an
617      * {@code IN} parameter.
618      *
619      * @author Thomas Darimont
620      */

621     static class InParameterBinding extends ParameterBinding {
622
623         /**
624          * Creates a new {@link InParameterBinding} for the parameter with the given name.
625          */

626         InParameterBinding(String name, @Nullable String expression) {
627             super(name, null, expression);
628         }
629
630         /**
631          * Creates a new {@link InParameterBinding} for the parameter with the given position.
632          */

633         InParameterBinding(int position, @Nullable String expression) {
634             super(null, position, expression);
635         }
636
637         /*
638          * (non-Javadoc)
639          * @see org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding#prepare(java.lang.Object)
640          */

641         @Override
642         public Object prepare(@Nullable Object value) {
643
644             if (!ObjectUtils.isArray(value)) {
645                 return value;
646             }
647
648             int length = Array.getLength(value);
649             Collection<Object> result = new ArrayList<>(length);
650
651             for (int i = 0; i < length; i++) {
652                 result.add(Array.get(value, i));
653             }
654
655             return result;
656         }
657     }
658
659     /**
660      * Represents a parameter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE
661      * parameter. This allows expressions like {@code …like %?1} in the JPQL query, which is not allowed by plain JPA.
662      *
663      * @author Oliver Gierke
664      * @author Thomas Darimont
665      */

666     static class LikeParameterBinding extends ParameterBinding {
667
668         private static final List<Type> SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH,
669                 Type.ENDING_WITH, Type.LIKE);
670
671         private final Type type;
672
673         /**
674          * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type}.
675          *
676          * @param name must not be {@literal null} or empty.
677          * @param type must not be {@literal null}.
678          */

679         LikeParameterBinding(String name, Type type) {
680             this(name, type, null);
681         }
682
683         /**
684          * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and parameter
685          * binding input.
686          *
687          * @param name must not be {@literal null} or empty.
688          * @param type must not be {@literal null}.
689          * @param expression may be {@literal null}.
690          */

691         LikeParameterBinding(String name, Type type, @Nullable String expression) {
692
693             super(name, null, expression);
694
695             Assert.hasText(name, "Name must not be null or empty!");
696             Assert.notNull(type, "Type must not be null!");
697
698             Assert.isTrue(SUPPORTED_TYPES.contains(type),
699                     String.format("Type must be one of %s!", StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
700
701             this.type = type;
702         }
703
704         /**
705          * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
706          *
707          * @param position position of the parameter in the query.
708          * @param type must not be {@literal null}.
709          */

710         LikeParameterBinding(int position, Type type) {
711             this(position, type, null);
712         }
713
714         /**
715          * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
716          *
717          * @param position position of the parameter in the query.
718          * @param type must not be {@literal null}.
719          * @param expression may be {@literal null}.
720          */

721         LikeParameterBinding(int position, Type type, @Nullable String expression) {
722
723             super(null, position, expression);
724
725             Assert.isTrue(position > 0, "Position must be greater than zero!");
726             Assert.notNull(type, "Type must not be null!");
727
728             Assert.isTrue(SUPPORTED_TYPES.contains(type),
729                     String.format("Type must be one of %s!", StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
730
731             this.type = type;
732         }
733
734         /**
735          * Returns the {@link Type} of the binding.
736          *
737          * @return the type
738          */

739         public Type getType() {
740             return type;
741         }
742
743         /**
744          * Prepares the given raw keyword according to the like type.
745          */

746         @Nullable
747         @Override
748         public Object prepare(@Nullable Object value) {
749
750             if (value == null) {
751                 return null;
752             }
753
754             switch (type) {
755                 case STARTING_WITH:
756                     return String.format("%s%%", value.toString());
757                 case ENDING_WITH:
758                     return String.format("%%%s", value.toString());
759                 case CONTAINING:
760                     return String.format("%%%s%%", value.toString());
761                 case LIKE:
762                 default:
763                     return value;
764             }
765         }
766
767         /*
768          * (non-Javadoc)
769          * @see java.lang.Object#equals(java.lang.Object)
770          */

771         @Override
772         public boolean equals(Object obj) {
773
774             if (!(obj instanceof LikeParameterBinding)) {
775                 return false;
776             }
777
778             LikeParameterBinding that = (LikeParameterBinding) obj;
779
780             return super.equals(obj) && this.type.equals(that.type);
781         }
782
783         /*
784          * (non-Javadoc)
785          * @see java.lang.Object#hashCode()
786          */

787         @Override
788         public int hashCode() {
789
790             int result = super.hashCode();
791
792             result += nullSafeHashCode(this.type);
793
794             return result;
795         }
796
797         /*
798          * (non-Javadoc)
799          * @see java.lang.Object#toString()
800          */

801         @Override
802         public String toString() {
803             return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type);
804         }
805
806         /**
807          * Extracts the like {@link Type} from the given JPA like expression.
808          *
809          * @param expression must not be {@literal null} or empty.
810          */

811         private static Type getLikeTypeFrom(String expression) {
812
813             Assert.hasText(expression, "Expression must not be null or empty!");
814
815             if (expression.matches("%.*%")) {
816                 return Type.CONTAINING;
817             }
818
819             if (expression.startsWith("%")) {
820                 return Type.ENDING_WITH;
821             }
822
823             if (expression.endsWith("%")) {
824                 return Type.STARTING_WITH;
825             }
826
827             return Type.LIKE;
828         }
829     }
830
831     static class Metadata {
832         private boolean usesJdbcStyleParameters = false;
833     }
834 }
835