View Javadoc
1   /*
2   * Copyright 2019-2026 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    *      http://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  
17  package org.bremersee.comparator.spring.mapper;
18  
19  import static java.util.Objects.isNull;
20  import static org.springframework.util.ObjectUtils.isEmpty;
21  
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.List;
25  import java.util.Objects;
26  import java.util.Optional;
27  import java.util.Set;
28  import java.util.stream.Collectors;
29  import java.util.stream.Stream;
30  import org.bremersee.comparator.model.SortOrder;
31  import org.bremersee.comparator.model.SortOrderItem;
32  import org.bremersee.comparator.model.SortOrderItem.CaseHandling;
33  import org.bremersee.comparator.model.SortOrderTextSeparators;
34  import org.bremersee.comparator.spring.converter.SortOrderConverter;
35  import org.jspecify.annotations.NonNull;
36  import org.jspecify.annotations.Nullable;
37  import org.springframework.data.domain.PageRequest;
38  import org.springframework.data.domain.Pageable;
39  import org.springframework.data.domain.Sort;
40  import org.springframework.data.domain.Sort.Direction;
41  import org.springframework.data.domain.Sort.NullHandling;
42  
43  /**
44   * This mapper provides methods to transform a {@link SortOrderItem} into a {@code Sort} object from
45   * the Spring framework (spring-data-common) and vice versa.
46   *
47   * @author Christian Bremer
48   */
49  public interface SortMapper {
50  
51    /**
52     * Returns default sort mapper.
53     *
54     * @return the sort mapper
55     */
56    static SortMapper defaultSortMapper() {
57      return defaultSortMapper(SortOrderTextSeparators.defaults());
58    }
59  
60    /**
61     * Returns default sort mapper.
62     *
63     * @param sortOrderTextSeparators the sort order text separators
64     * @return the sort mapper
65     */
66    static SortMapper defaultSortMapper(SortOrderTextSeparators sortOrderTextSeparators) {
67      return defaultSortMapper(new SortOrderConverter(sortOrderTextSeparators));
68    }
69  
70    /**
71     * Returns default sort mapper.
72     *
73     * @param sortOrderConverter the sort order converter
74     * @return the sort mapper
75     */
76    static SortMapper defaultSortMapper(SortOrderConverter sortOrderConverter) {
77      return new DefaultSortMapper(sortOrderConverter);
78    }
79  
80    /**
81     * Gets sort order from text.
82     *
83     * @param sortOrderText the sort order text
84     * @return the sort order
85     */
86    SortOrder getSortOrder(@Nullable String sortOrderText);
87  
88    /**
89     * Gets sort order text.
90     *
91     * @param sortOrder the sort order
92     * @return the sort order text
93     */
94    String getSortOrderText(@Nullable SortOrder sortOrder);
95  
96    /**
97     * Gets sort order text of items.
98     *
99     * @param sortOrder the sort order
100    * @return the sort order item text
101    */
102   List<String> getSortOrderItemText(@Nullable SortOrder sortOrder);
103 
104   /**
105    * Transforms sort order into a {@code Sort} object.
106    *
107    * @param sortOrder the sort order
108    * @return the sort
109    */
110   @NonNull
111   default Sort toSort(@Nullable SortOrder sortOrder) {
112     List<Sort.Order> orderList = Stream.ofNullable(sortOrder)
113         .map(SortOrder::getItems)
114         .flatMap(Collection::stream)
115         .filter(Objects::nonNull)
116         .map(this::toSortOrder)
117         .toList();
118     return orderList.isEmpty() ? Sort.unsorted() : Sort.by(orderList);
119   }
120 
121   /**
122    * Transforms the sort order into a {@code Sort.Order} object.
123    *
124    * @param sortOrderItem the sort order
125    * @return the sort object
126    */
127   default Sort.Order toSortOrder(@Nullable SortOrderItem sortOrderItem) {
128     if (sortOrderItem == null || sortOrderItem.getField() == null) {
129       return null;
130     }
131     Direction direction = sortOrderItem.getDirection().isAscending()
132         ? Direction.ASC
133         : Direction.DESC;
134     NullHandling nullHandling = switch (sortOrderItem.getNullHandling()) {
135       case NULLS_FIRST -> NullHandling.NULLS_FIRST;
136       case NULLS_LAST -> NullHandling.NULLS_LAST;
137       case NATIVE -> NullHandling.NATIVE;
138     };
139     Sort.Order order = new Sort.Order(direction, sortOrderItem.getField(), nullHandling);
140     return sortOrderItem.getCaseHandling().isInsensitive()
141         ? order.ignoreCase()
142         : order;
143   }
144 
145 
146   /**
147    * Transforms a {@code Sort} object into a sort order.
148    *
149    * @param sort the {@code Sort} object
150    * @return the sort order
151    */
152   @NonNull
153   default SortOrder fromSort(@Nullable Sort sort) {
154     List<SortOrderItem> items = Stream.ofNullable(sort)
155         .flatMap(Sort::stream)
156         .map(this::fromSortOrder)
157         .filter(Objects::nonNull)
158         .toList();
159     return new SortOrder(items);
160   }
161 
162   /**
163    * Transforms a {@code Sort.Order} object into a sort order.
164    *
165    * @param sortOrder the {@code Sort.Order} object
166    * @return the sort order
167    */
168   @Nullable
169   default SortOrderItem fromSortOrder(Sort.Order sortOrder) {
170     if (isNull(sortOrder)) {
171       return null;
172     }
173     SortOrderItem.Direction direction = sortOrder.getDirection().isAscending()
174         ? SortOrderItem.Direction.ASC
175         : SortOrderItem.Direction.DESC;
176     CaseHandling caseHandling = sortOrder.isIgnoreCase()
177         ? CaseHandling.INSENSITIVE
178         : CaseHandling.SENSITIVE;
179     SortOrderItem.NullHandling nullHandling = switch (sortOrder.getNullHandling()) {
180       case NULLS_FIRST -> SortOrderItem.NullHandling.NULLS_FIRST;
181       case NULLS_LAST -> SortOrderItem.NullHandling.NULLS_LAST;
182       case NATIVE -> SortOrderItem.NullHandling.NATIVE;
183     };
184     return new SortOrderItem(
185         sortOrder.getProperty(),
186         direction,
187         caseHandling,
188         nullHandling);
189   }
190 
191   /**
192    * Apply defaults to page request.
193    *
194    * @param source the source
195    * @param direction the direction
196    * @param ignoreCase the ignore case
197    * @param nullHandling the null handling
198    * @param properties the properties
199    * @return the pageable
200    */
201   @Nullable
202   default Pageable applyDefaults(
203       @Nullable Pageable source,
204       @Nullable Direction direction,
205       @Nullable Boolean ignoreCase,
206       @Nullable NullHandling nullHandling,
207       @Nullable String... properties) {
208 
209     return isNull(source) ? null : PageRequest.of(
210         source.getPageNumber(),
211         source.getPageSize(),
212         applyDefaults(source.getSort(), direction, ignoreCase, nullHandling, properties));
213   }
214 
215   /**
216    * Apply defaults to sort.
217    *
218    * @param source the source
219    * @param direction the direction
220    * @param ignoreCase the ignore case
221    * @param nullHandling the null handling
222    * @param properties the properties
223    * @return the sort
224    */
225   @NonNull
226   default Sort applyDefaults(
227       @Nullable Sort source,
228       @Nullable Direction direction,
229       @Nullable Boolean ignoreCase,
230       @Nullable NullHandling nullHandling,
231       @Nullable String... properties) {
232 
233     if (isNull(source)) {
234       return Sort.unsorted();
235     }
236     if (isNull(direction) && isNull(ignoreCase) && isNull(nullHandling)) {
237       return source;
238     }
239     Set<String> names;
240     if (isEmpty(properties)) {
241       names = source.stream().map(Sort.Order::getProperty).collect(Collectors.toSet());
242     } else {
243       names = Arrays.stream(properties).collect(Collectors.toSet());
244     }
245     return Sort.by(source.stream()
246         .map(sortOrder -> {
247           if (names.contains(sortOrder.getProperty())) {
248             Sort.Order order = Sort.Order.by(sortOrder.getProperty())
249                 .with(newDirection(sortOrder.getDirection(), direction))
250                 .with(newNullHandling(sortOrder.getNullHandling(), nullHandling));
251             return withNewCaseHandling(order, sortOrder.isIgnoreCase(), ignoreCase);
252           }
253           return sortOrder;
254         })
255         .toList());
256   }
257 
258   private Direction newDirection(Direction oldDirection, Direction newDirection) {
259     return Optional.ofNullable(newDirection)
260         .orElse(oldDirection);
261   }
262 
263   private NullHandling newNullHandling(NullHandling oldNullHandling, NullHandling newNullHandling) {
264     return Optional.ofNullable(newNullHandling)
265         .orElse(oldNullHandling);
266   }
267 
268   private Sort.Order withNewCaseHandling(
269       Sort.Order order,
270       boolean oldIgnoresCase,
271       Boolean newIgnoresCase) {
272     //noinspection ConstantConditions
273     return Optional.ofNullable(newIgnoresCase)
274         .map(ignoreCase -> ignoreCase ? order.ignoreCase() : order)
275         .orElseGet(() -> oldIgnoresCase ? order.ignoreCase() : order);
276   }
277 
278   /**
279    * The default sort mapper.
280    */
281   @SuppressWarnings("ClassCanBeRecord")
282   class DefaultSortMapper implements SortMapper {
283 
284     private final SortOrderConverter converter;
285 
286     /**
287      * Instantiates a new default sort mapper.
288      *
289      * @param converter the converter
290      */
291     DefaultSortMapper(SortOrderConverter converter) {
292       this.converter = Objects.requireNonNullElseGet(converter, SortOrderConverter::new);
293     }
294 
295     @Override
296     public SortOrder getSortOrder(@Nullable String sortOrderText) {
297       if (isNull(sortOrderText)) {
298         return SortOrder.unsorted();
299       }
300       return converter.convert(sortOrderText);
301     }
302 
303     @Override
304     public String getSortOrderText(@Nullable SortOrder sortOrder) {
305       if (isNull(sortOrder)) {
306         return null;
307       }
308       if (sortOrder.isEmpty()) {
309         return "";
310       }
311       return sortOrder.getSortOrderText(converter.getSeparators());
312     }
313 
314     @Override
315     public List<String> getSortOrderItemText(SortOrder sortOrder) {
316       return Stream.ofNullable(sortOrder)
317           .map(SortOrder::getItems)
318           .filter(Objects::nonNull)
319           .flatMap(Collection::stream)
320           .map(item -> item.getSortOrderText(converter.getSeparators()))
321           .filter(Objects::nonNull)
322           .toList();
323     }
324 
325   }
326 
327 }