10

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?

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
wheeleruniverse
  • 1,511
  • 2
  • 20
  • 37
  • 3
    Really, you're re-implementing a database here. The only way to do this cleanly, really, is by using reflection to generate a lambda with a `MethodReference`. You could potentially create an `enum` and use polymorphism - but that only solves the immediate problem; it wouldn't help with future extensibility. – Boris the Spider Aug 19 '17 at 16:25
  • 3
    Note, for an enterprise type application; parsing user data is an area that needs careful security review - so just naked reflection would not be good enough. – Boris the Spider Aug 19 '17 at 16:28
  • Wouldn't reflection be fine though if it took place in running code that's on a server protected by a firewall? – wheeleruniverse Aug 19 '17 at 16:30
  • 4
    But if you're taking user input and turning that into a method call, that is dangerous. For example `field="shutDown"` or `clear` or whatever. Reflection will indiscriminately call what ever method it's told to. You would still need to keep a catalogue of allowed sorts; perhaps using method annotations that could be checked during reflection or some such. – Boris the Spider Aug 19 '17 at 16:32
  • 4
    You could, using annotations on sortable methods, use an [annotation processor](http://hannesdorfmann.com/annotation-processing/annotationprocessing101) to generate code at compile time to do the sorting, this would also generate some way to lookup the appropriate sorter. This is rather advanced however - if you're not an experienced developer it may not be a technique that's achievable. – Boris the Spider Aug 19 '17 at 16:33
  • Well the input was expected to come from the Front-end developers not really the UI, but I will look into this annotation processor and see if it's something that will work for me. – wheeleruniverse Aug 19 '17 at 16:39
  • 3
    Yes, but using any browser's developer tools I can see the content of information going over the wire. I can then make arbitrary changes to it to see how the application reacts. Trusting any input to your application without verification is a serious security problem - regardless of where it comes from. Trust me, in an enterprise environment - this will fail security review. – Boris the Spider Aug 19 '17 at 16:42

4 Answers4

11

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.

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
  • 3
    Good for a medium term solution. Moderately extensible, and easily testable. Provides a way to easily validate input to some extent, and the map keys could be namespaced (e.g. `movie.id`) to allow different types of objects to be sorted. +1 – Boris the Spider Aug 19 '17 at 16:48
  • Thank you for the input. I think I am going to experiment with the Annotation Processor like Boris recommended. I am familiar with Annotations already so I think if I can just annotate all the classes I want to be sortable with the fields that I will allow to be sorted I can generate the java code from those. – wheeleruniverse Aug 19 '17 at 17:02
  • 1
    @jDub9, I'll help you out and add a solution a bit later. You will be able to compare your approach with mine. – Andrew Tobilko Aug 19 '17 at 17:22
  • 3
    This is good code, +1. However, I think you are reinventing the wheel here. I'd use the excellent [Reflections library](https://github.com/ronmamo/reflections) for this, which lets you scan your whole classpath for annotated methods. Then, you could create the functions from there. – fps Aug 20 '17 at 03:04
  • @AndrewTobilko if you are creating a `Map`, then create it to have the values as `Comparator` directly - this way you can leverage the `comparingInt`, `comparingLong`, etc – Eugene Aug 20 '17 at 11:01
  • @Eugene, I'm using the `comparing(Function)`, so instead of creating `n` `Comparator`s with the map, I'm creating only one. – Andrew Tobilko Aug 20 '17 at 11:14
3

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

Eugene
  • 117,005
  • 15
  • 201
  • 306
1

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)));
}
Serhii Korol
  • 843
  • 7
  • 15
0

Consider using ComparatorChain from apache commons.

Take a look to this answer https://stackoverflow.com/a/20093642/3790546 to see how to use it.

Pablo
  • 307
  • 3
  • 8