Spring Data's Immutable Sort and ignoreCase

While doing some functional testing on an application I noticed that the sort of my collections coming back from the database weren't quite right; they were case sensitive. For example, what I wanted was:

  • Apple
  • apricot
  • Lemon
  • Orange

But what I got was

  • Apple
  • Lemon
  • Orange
  • apricot

The culprit, if you want to call it that, is the Pageable interface. Given the following repository definition:

@Repository
public interface FruitRepository extends PagingAndSortingRepository<Fruit, Long> {  
    Page<Fruit> findAll(Pageable pageable);
}

And the following controller method:

public ResponseEntity<List<Fruit>> fetchAll(Pageable pageable) {  
    Page fruit = this.fruitRepository.findAll(pageable);
    return new ResponseEntity<>(fruit.getContent(), HttpStatus.OK);
}

I wanted to find a global way of making the sort case-insensitive. Sure, I could have just sorted the data again, I could have locked myself into the database vendor doing it there or I could have manually created a PageRequest but global and portable is key here.

Under the hood, when a request comes into the controller method, a PageRequest is constructed. The PageRequest can accept a Sort object and the Sort object has an Order which has a method ignoreCase(). The interesting part is that Sort.Order is immutable so when you call the ignoreCase() method, this is what happens:

/**
* Returns a new {@link Order} with case insensitive sorting enabled.
* 
* @return
*/
public Order ignoreCase() {  
    return new Order(direction, property, true, nullHandling);
}

After a lot of documentation reading and googling, assuming what I'd find is a configuration switch to toggle case sensitivity, the best I found was a link on Stackoverflow with a suggestion on using an Aspect. Unfortunately, as written, the Aspect didn't work. This line:

return (arg) -> arg instanceof Sort...

was never true because the arg was never Sort. It was, however, PageRequest. So with some modification, some help from a colleague and quite a bit of testing, I ended up with the following:

import org.aspectj.lang.ProceedingJoinPoint;  
import org.aspectj.lang.annotation.Around;  
import org.aspectj.lang.annotation.Aspect;  
import org.springframework.data.domain.PageRequest;  
import org.springframework.data.domain.Sort;  
import org.springframework.stereotype.Component;

import java.util.Arrays;  
import java.util.List;  
import java.util.stream.Collectors;  
import java.util.stream.StreamSupport;

@Aspect
@Component
public class SortManipulatingAspect {

    @Around("execution(public * org.springframework.data.repository.PagingAndSortingRepository+.*(..))")
    public Object enableIgnoreCaseSorting(ProceedingJoinPoint joinPoint) throws Throwable {

        return joinPoint.proceed(
                Arrays.stream(joinPoint.getArgs()).map(SortManipulatingAspect::sortWithIgnoreCase).toArray()
        );
    }

    private static Object sortWithIgnoreCase(Object arg) {
        if (arg instanceof PageRequest) {
            return pageRequestIgnoreCaseSort((PageRequest)arg);
        } else {
            return arg;
        }

    }

    private static PageRequest pageRequestIgnoreCaseSort(PageRequest pageRequest) {
        return new PageRequest(
                pageRequest.getPageNumber(),
                pageRequest.getPageSize(),
                pageRequest.getSort() != null ? new Sort(toOrderStream(pageRequest.getSort())) : null
        );
    }

    private static List<Sort.Order> toOrderStream(Sort sort) {
        return StreamSupport.stream(sort.spliterator(), false)
                .map(Sort.Order::ignoreCase)
                .collect(Collectors.toList());
    }

}

Honestly, while I understand what is happening, I don't fully understand Aspects so I'm still grocking my head around it. The next step would be to be able to toggle this Aspect so that I could use it when I cared about it, but that's for another day. I'm really surprised it was this difficult to find a solution. It would seem that case-sensitivity on sorting would be easier to modify without having to drop out of the interfaces that Spring provides in Pageable. If I'm missing something, please let me know.