Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sorting with Java 8 by Field given as Input

I have a REST endpoint and I want the UI to pass the field name that they want to sort their result by "id", "name", etc. I came up with below, but was really trying to use Reflection / Generics so this could be expanded to encompass every object in my project.

I feel like this solution isn't easily maintainable if I want to have the same functionality for 100 different classes.

public static void sort(List<MovieDTO> collection, String field) {
    if(collection == null || collection.size() < 1 || field == null || field.isEmpty()) {
        return;
    }

    switch(field.trim().toLowerCase()) {
        case "id": 
            collection.sort(Comparator.comparing(MovieDTO::getId));
            break;
        case "name": 
            collection.sort(Comparator.comparing(MovieDTO::getName));
            break;
        case "year": 
            collection.sort(Comparator.comparing(MovieDTO::getYear));
            break;
        case "rating": 
            collection.sort(Comparator.comparing(MovieDTO::getRating));
            break;
        default: 
            collection.sort(Comparator.comparing(MovieDTO::getId));
            break;
    }
}

Any ideas on how I could implement this better so that it can be expanded to work for an enterprise application with little maintenance?

like image 812
wheelerswebservices Avatar asked Aug 19 '17 16:08

wheelerswebservices


3 Answers

Original post

I won't repeat everything said in the comments. There are good thoughts there. I hope you understand that reflection is not an optimal choice here.

I would suggest keeping a Map<String, Function<MovieDTO, String>>, where the key is a field name, the value is a mapper movie -> field:

Map<String, Function<MovieDTO, String>> extractors = ImmutableMap.of(
        "id", MovieDTO::getId,
        "name", MovieDTO::getName
);

Then, the collection can be sorted like:

Function<MovieDTO, String> extractor = extractors.getOrDefault(
        field.trim().toLowerCase(),
        MovieDTO::getId
);
collection.sort(Comparator.comparing(extractor));

Playing with reflection

As I promised, I am adding my vision of annotation processing to help you out. Note, it's not a version you have to stick firmly. It's rather a good point to start with.

I declared 2 annotations.

  • To clarify a getter name ( if not specified, <get + FieldName> is the pattern):

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD})
    @interface FieldExtractor {
    
        String getterName();
    
    }
    
  • To define all possible sorting keys for a class:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @interface SortingFields {
    
        String[] fields();
    
    }
    

The class MovieDTO has been given the following look:

@SortingFields(fields = {"id", "name"})
class MovieDTO implements Comparable<MovieDTO> {

    @FieldExtractor(getterName = "getIdentifier")
    private Long id;
    private String name;

    public Long getIdentifier() {
        return id;
    }

    public String getName() {
        return name;
    }

    ...

}

I didn't change the sort method signature (though, it would simplify the task):

public static <T> void sort(List<T> collection, String field) throws NoSuchMethodException, NoSuchFieldException {
    if (collection == null || collection.isEmpty() || field == null || field.isEmpty()) {
        return;
    }

    // get a generic type of the collection
    Class<?> genericType = ActualGenericTypeExtractor.extractFromType(collection.getClass().getGenericSuperclass());

    // get a key-extractor function
    Function<T, Comparable<? super Object>> extractor = SortingKeyExtractor.extractFromClassByFieldName(genericType, field);

    // sort
    collection.sort(Comparator.comparing(extractor));
}

As you may see, I needed to introduce 2 classes to accomplish:

class ActualGenericTypeExtractor {

    public static Class<?> extractFromType(Type type) {
        // check if it is a waw type
        if (!(type instanceof ParameterizedType)) {
            throw new IllegalArgumentException("Raw type has been found! Specify a generic type for further scanning.");
        }

        // return the first generic type
        return (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
    }

}

class SortingKeyExtractor {

    @SuppressWarnings("unchecked")
    public static <T> Function<T, Comparable<? super Object>> extractFromClassByFieldName(Class<?> type, String fieldName) throws NoSuchFieldException, NoSuchMethodException {
        // check if the fieldName is in allowed fields
        validateFieldName(type, fieldName);

        // fetch a key-extractor method
        Method method = findExtractorForField(type, type.getDeclaredField(fieldName));

        // form a Function with a method invocation inside
        return (T instance) -> {
            try {
                return (Comparable<? super Object>) method.invoke(instance);
            } catch (IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
            return null;
        };
    }

    private static Method findExtractorForField(Class<?> type, Field field) throws NoSuchMethodException {
        // generate the default name for a getter
        String fieldName = "get" + StringUtil.capitalize(field.getName());

        // override it if specified by the annotation
        if (field.isAnnotationPresent(FieldExtractor.class)) {
            fieldName = field.getAnnotation(FieldExtractor.class).getterName();
        }

        System.out.println("> Fetching a method with the name [" + fieldName + "]...");

        return type.getDeclaredMethod(fieldName);
    }

    private static void validateFieldName(Class<?> type, String fieldName) {
        if (!type.isAnnotationPresent(SortingFields.class)) {
            throw new IllegalArgumentException("A list of sorting fields hasn't been specified!");
        }

        SortingFields annotation = type.getAnnotation(SortingFields.class);

        for (String field : annotation.fields()) {
            if (field.equals(fieldName)) {
                System.out.println("> The given field name [" + fieldName + "] is allowed!");
                return;
            }
        }

        throw new IllegalArgumentException("The given field is not allowed to be a sorting key!");
    }

}

It looks a bit complicated, but it's the price for generalisation. Of course, there is room for improvements, and if you pointed them out, I would be glad to look over.

like image 164
Andrew Tobilko Avatar answered Oct 26 '22 13:10

Andrew Tobilko


Well, you could create a Function that would be generic for your types:

private static <T, R> Function<T, R> findFunction(Class<T> clazz, String fieldName, Class<R> fieldType) throws Throwable {

    MethodHandles.Lookup caller = MethodHandles.lookup();
    MethodType getter = MethodType.methodType(fieldType);
    MethodHandle target = caller.findVirtual(clazz, "get" + fieldName, getter);
    MethodType func = target.type();
    CallSite site = LambdaMetafactory.metafactory(caller,
            "apply",
            MethodType.methodType(Function.class),
            func.erase(),
            target,
            func);

    MethodHandle factory = site.getTarget();
    Function<T, R> function = (Function<T, R>) factory.invoke();

    return function;
}

The only problem is that you need to know the types, via the last parameter fieldType

like image 37
Eugene Avatar answered Oct 26 '22 13:10

Eugene


I'd use jOOR library and the following snippet:

public static <T, U extends Comparable<U>> void sort(final List<T> collection, final String fieldName) {
    collection.sort(comparing(ob -> (U) on(ob).get(fieldName)));
}
like image 45
Sergey Korol Avatar answered Oct 26 '22 12:10

Sergey Korol