1 /*
2 * Copyright 2008-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.repository.query.parser;
17
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.HashSet;
21 import java.util.List;
22 import java.util.Optional;
23 import java.util.Set;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import org.springframework.data.domain.Sort;
28 import org.springframework.data.domain.Sort.Direction;
29 import org.springframework.data.domain.Sort.Order;
30 import org.springframework.data.mapping.PropertyPath;
31 import org.springframework.util.StringUtils;
32
33 /**
34 * Simple helper class to create a {@link Sort} instance from a method name end. It expects the last part of the method
35 * name to be given and supports lining up multiple properties ending with the sorting direction. So the following
36 * method ends are valid: {@code LastnameUsernameDesc}, {@code LastnameAscUsernameDesc}.
37 *
38 * @author Oliver Gierke
39 * @author Christoph Strobl
40 * @author Mark Paluch
41 */
42 class OrderBySource {
43
44 static OrderBySource EMPTY = new OrderBySource("");
45
46 private static final String BLOCK_SPLIT = "(?<=Asc|Desc)(?=\\p{Lu})";
47 private static final Pattern DIRECTION_SPLIT = Pattern.compile("(.+?)(Asc|Desc)?$");
48 private static final String INVALID_ORDER_SYNTAX = "Invalid order syntax for part %s!";
49 private static final Set<String> DIRECTION_KEYWORDS = new HashSet<>(Arrays.asList("Asc", "Desc"));
50
51 private final List<Order> orders;
52
53 /**
54 * Creates a new {@link OrderBySource} for the given String clause not doing any checks whether the referenced
55 * property actually exists.
56 *
57 * @param clause must not be {@literal null}.
58 */
59 OrderBySource(String clause) {
60 this(clause, Optional.empty());
61 }
62
63 /**
64 * Creates a new {@link OrderBySource} for the given clause, checking the property referenced exists on the given
65 * type.
66 *
67 * @param clause must not be {@literal null}.
68 * @param domainClass must not be {@literal null}.
69 */
70 OrderBySource(String clause, Optional<Class<?>> domainClass) {
71
72 this.orders = new ArrayList<>();
73
74 if (!StringUtils.hasText(clause)) {
75 return;
76 }
77
78 for (String part : clause.split(BLOCK_SPLIT)) {
79
80 Matcher matcher = DIRECTION_SPLIT.matcher(part);
81
82 if (!matcher.find()) {
83 throw new IllegalArgumentException(String.format(INVALID_ORDER_SYNTAX, part));
84 }
85
86 String propertyString = matcher.group(1);
87 String directionString = matcher.group(2);
88
89 // No property, but only a direction keyword
90 if (DIRECTION_KEYWORDS.contains(propertyString) && directionString == null) {
91 throw new IllegalArgumentException(String.format(INVALID_ORDER_SYNTAX, part));
92 }
93
94 this.orders.add(createOrder(propertyString, Direction.fromOptionalString(directionString), domainClass));
95 }
96 }
97
98 /**
99 * Creates an {@link Order} instance from the given property source, direction and domain class. If the domain class
100 * is given, we will use it for nested property traversal checks.
101 *
102 * @param propertySource
103 * @param direction must not be {@literal null}.
104 * @param domainClass must not be {@literal null}.
105 * @return
106 * @see PropertyPath#from(String, Class)
107 */
108 private Order createOrder(String propertySource, Optional<Direction> direction, Optional<Class<?>> domainClass) {
109
110 return domainClass.map(type -> {
111
112 PropertyPath propertyPath = PropertyPath.from(propertySource, type);
113 return direction.map(it -> new Order(it, propertyPath.toDotPath()))
114 .orElseGet(() -> Order.by(propertyPath.toDotPath()));
115
116 }).orElseGet(() -> direction//
117 .map(it -> new Order(it, StringUtils.uncapitalize(propertySource)))
118 .orElseGet(() -> Order.by(StringUtils.uncapitalize(propertySource))));
119 }
120
121 /**
122 * Returns the clause as {@link Sort}.
123 *
124 * @return the {@link Sort}.
125 */
126 Sort toSort() {
127 return Sort.by(this.orders);
128 }
129
130 /*
131 * (non-Javadoc)
132 * @see java.lang.Object#toString()
133 */
134 @Override
135 public String toString() {
136 return "Order By " + StringUtils.collectionToDelimitedString(orders, ", ");
137 }
138 }
139