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?
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));
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.
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
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)));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With