View Javadoc
1   /*
2    * Copyright 2019-2022 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.springframework.data.domain.PageRequest;
36  import org.springframework.data.domain.Pageable;
37  import org.springframework.data.domain.Sort;
38  import org.springframework.data.domain.Sort.Direction;
39  import org.springframework.data.domain.Sort.NullHandling;
40  import org.springframework.lang.NonNull;
41  import org.springframework.lang.Nullable;
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   @Nullable
128   default Sort.Order toSortOrder(@Nullable SortOrderItem sortOrderItem) {
129     if (sortOrderItem == null || sortOrderItem.getField() == null) {
130       return null;
131     }
132     Direction direction = sortOrderItem.getDirection().isAscending()
133         ? Direction.ASC
134         : Direction.DESC;
135     NullHandling nullHandling = switch (sortOrderItem.getNullHandling()) {
136       case NULLS_FIRST -> NullHandling.NULLS_FIRST;
137       case NULLS_LAST -> NullHandling.NULLS_LAST;
138       case NATIVE -> NullHandling.NATIVE;
139     };
140     Sort.Order order = new Sort.Order(direction, sortOrderItem.getField(), nullHandling);
141     return sortOrderItem.getCaseHandling().isInsensitive()
142         ? order.ignoreCase()
143         : order;
144   }
145 
146 
147   /**
148    * Transforms a {@code Sort} object into a sort order.
149    *
150    * @param sort the {@code Sort} object
151    * @return the sort order
152    */
153   @NonNull
154   default SortOrder fromSort(@Nullable Sort sort) {
155     List<SortOrderItem> items = Stream.ofNullable(sort)
156         .flatMap(Sort::stream)
157         .map(this::fromSortOrder)
158         .filter(Objects::nonNull)
159         .toList();
160     return new SortOrder(items);
161   }
162 
163   /**
164    * Transforms a {@code Sort.Order} object into a sort order.
165    *
166    * @param sortOrder the {@code Sort.Order} object
167    * @return the sort order
168    */
169   @Nullable
170   default SortOrderItem fromSortOrder(@Nullable Sort.Order sortOrder) {
171     if (isNull(sortOrder)) {
172       return null;
173     }
174     SortOrderItem.Direction direction = sortOrder.getDirection().isAscending()
175         ? SortOrderItem.Direction.ASC
176         : SortOrderItem.Direction.DESC;
177     CaseHandling caseHandling = sortOrder.isIgnoreCase()
178         ? CaseHandling.INSENSITIVE
179         : CaseHandling.SENSITIVE;
180     SortOrderItem.NullHandling nullHandling = switch (sortOrder.getNullHandling()) {
181       case NULLS_FIRST -> SortOrderItem.NullHandling.NULLS_FIRST;
182       case NULLS_LAST -> SortOrderItem.NullHandling.NULLS_LAST;
183       case NATIVE -> SortOrderItem.NullHandling.NATIVE;
184     };
185     return new SortOrderItem(
186         sortOrder.getProperty(),
187         direction,
188         caseHandling,
189         nullHandling);
190   }
191 
192   /**
193    * Apply defaults to page request.
194    *
195    * @param source the source
196    * @param direction the direction
197    * @param ignoreCase the ignore case
198    * @param nullHandling the null handling
199    * @param properties the properties
200    * @return the pageable
201    */
202   @Nullable
203   default Pageable applyDefaults(
204       @Nullable Pageable source,
205       @Nullable Direction direction,
206       @Nullable Boolean ignoreCase,
207       @Nullable NullHandling nullHandling,
208       @Nullable String... properties) {
209 
210     return isNull(source) ? null : PageRequest.of(
211         source.getPageNumber(),
212         source.getPageSize(),
213         applyDefaults(source.getSort(), direction, ignoreCase, nullHandling, properties));
214   }
215 
216   /**
217    * Apply defaults to sort.
218    *
219    * @param source the source
220    * @param direction the direction
221    * @param ignoreCase the ignore case
222    * @param nullHandling the null handling
223    * @param properties the properties
224    * @return the sort
225    */
226   @NonNull
227   default Sort applyDefaults(
228       @Nullable Sort source,
229       @Nullable Direction direction,
230       @Nullable Boolean ignoreCase,
231       @Nullable NullHandling nullHandling,
232       @Nullable String... properties) {
233 
234     if (isNull(source)) {
235       return Sort.unsorted();
236     }
237     if (isNull(direction) && isNull(ignoreCase) && isNull(nullHandling)) {
238       return source;
239     }
240     Set<String> names;
241     if (isEmpty(properties)) {
242       names = source.stream().map(Sort.Order::getProperty).collect(Collectors.toSet());
243     } else {
244       names = Arrays.stream(properties).collect(Collectors.toSet());
245     }
246     return Sort.by(source.stream()
247         .map(sortOrder -> {
248           if (names.contains(sortOrder.getProperty())) {
249             Sort.Order order = Sort.Order.by(sortOrder.getProperty())
250                 .with(newDirection(sortOrder.getDirection(), direction))
251                 .with(newNullHandling(sortOrder.getNullHandling(), nullHandling));
252             return withNewCaseHandling(order, sortOrder.isIgnoreCase(), ignoreCase);
253           }
254           return sortOrder;
255         })
256         .toList());
257   }
258 
259   private Direction newDirection(Direction oldDirection, Direction newDirection) {
260     return Optional.ofNullable(newDirection)
261         .orElse(oldDirection);
262   }
263 
264   private NullHandling newNullHandling(NullHandling oldNullHandling, NullHandling newNullHandling) {
265     return Optional.ofNullable(newNullHandling)
266         .orElse(oldNullHandling);
267   }
268 
269   private Sort.Order withNewCaseHandling(
270       Sort.Order order,
271       boolean oldIgnoresCase,
272       Boolean newIgnoresCase) {
273     //noinspection ConstantConditions
274     return Optional.ofNullable(newIgnoresCase)
275         .map(ignoreCase -> ignoreCase ? order.ignoreCase() : order)
276         .orElseGet(() -> oldIgnoresCase ? order.ignoreCase() : order);
277   }
278 
279   /**
280    * The default sort mapper.
281    */
282   @SuppressWarnings("ClassCanBeRecord")
283   class DefaultSortMapper implements SortMapper {
284 
285     private final SortOrderConverter converter;
286 
287     /**
288      * Instantiates a new default sort mapper.
289      *
290      * @param converter the converter
291      */
292     DefaultSortMapper(SortOrderConverter converter) {
293       this.converter = Objects.requireNonNullElseGet(converter, SortOrderConverter::new);
294     }
295 
296     @Override
297     public SortOrder getSortOrder(@Nullable String sortOrderText) {
298       if (isNull(sortOrderText)) {
299         return SortOrder.unsorted();
300       }
301       return converter.convert(sortOrderText);
302     }
303 
304     @Override
305     public String getSortOrderText(@Nullable SortOrder sortOrder) {
306       if (isNull(sortOrder)) {
307         return null;
308       }
309       if (sortOrder.isEmpty()) {
310         return "";
311       }
312       return sortOrder.getSortOrderText(converter.getSeparators());
313     }
314 
315     @Override
316     public List<String> getSortOrderItemText(SortOrder sortOrder) {
317       return Stream.ofNullable(sortOrder)
318           .map(SortOrder::getItems)
319           .filter(Objects::nonNull)
320           .flatMap(Collection::stream)
321           .map(item -> item.getSortOrderText(converter.getSeparators()))
322           .filter(Objects::nonNull)
323           .toList();
324     }
325 
326   }
327 
328 }