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