Reputation: 27909
The following method performs ordering.
public List<Comparator<Entity>> sort(Map<String, String> map) {
List<Comparator<Entity>> list = new ArrayList<Comparator<Entity>>();
for (Map.Entry<String, String> entry : map.entrySet()) {
boolean sortOrder = entry.getValue().equalsIgnoreCase("asc");
switch (entry.getKey()) {
case "id":
list.add(sortOrder ? Comparator.comparing(Entity::getId) : Comparator.comparing(Entity::getId, Comparator.reverseOrder()));
break;
case "size":
list.add(sortOrder ? Comparator.comparing(Entity::getSize) : Comparator.comparing(Entity::getSize, Comparator.reverseOrder()));
//break;
}
}
return list;
}
The list being returned by the above method is used in the following way.
// map is initialized somewhere based on client's interactions with sorting.
// Based on client's interactions, map may be empty or it may contain one or more ordering fields.
if (MapUtils.isNotEmpty(map)) { // map = new LinkedHashMap<String, String>();
List<Comparator<Entity>> comparators = sort(map);
Comparator<Entity> comparator = comparators.remove(0);
for (Comparator<Entity> c : comparators) {
comparator = comparator.thenComparing(c);
}
list = list.stream().sorted(comparator).collect(Collectors.toList());
} else {
// This is the default ordering.
list = list.stream().sorted(Comparator.comparing(Entity::getId).reversed()).collect(Collectors.toList());
}
Entity
contains two fields named id
of type Integer
and size
of type BigDecimal
and list
is a type of List<Entity>
.
Since there are several other classes having the same fields with the same datatypes, I want this method to be generic so that it has to be defined only once like so,
public <T extends Object> List<Comparator<T>> sort(Map<String, String> map, Class<T> clazz) {
List<Comparator<T>> list = new ArrayList<Comparator<T>>();
// Sorting logic.
return list;
}
But doing so, expressions like T::getId
will not compile as obvious, since the generic type parameter T
evaluates to Object
.
Is there a way to code sorting without knowing the actual class type so that this method can be prevented from being repeated everywhere, when it is needed?
Upvotes: 2
Views: 2749
Reputation: 298559
If you want to create a compound Comparator
anyway, there is no point in filling a List
first. Just do it in one operation:
public static <T> Comparator<T> getOrdering(
Map<String, String> map, Map<String,Comparator<T>> defined) {
return map.entrySet().stream().map(e -> {
Comparator<T> c=defined.get(e.getKey());
return e.getValue().equalsIgnoreCase("asc")? c: c.reversed();
})
.reduce(Comparator::thenComparing)
.orElseThrow(()->new IllegalArgumentException("empty"));
}
This works for arbitrary types but requires to provide a map of existing comparators for a type. But this map
isn’t a restriction, it actually improves the operation as it removes the hardcoded set of existing named property comparators. You can use it with an arbitrary type, Entity
being exemplary here, as follows:
Map<String,Comparator<Entity>> map=new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
map.put("id", Comparator.comparing(Entity::getID));
map.put("size", Comparator.comparing(Entity::getSize));
Comparator<Entity> cmp=getOrdering(param, map);
whereas param
is the ordered map of your question, mapping from property name to either "asc"
or "desc"
. The map
holding the predefined comparators can be created once in initialization code and then be re-used.
The creation code doesn’t look so complicated that it deserves implementing a dynamic solution, however, if you still wish to do it, here is the code to generate such a map for arbitrary classes:
public final class DynamicComparators<T> {
public static <T> Map<String,Comparator<T>> getComparators(Class<T> cl) {
return CACHE.get(cl).cast(cl).comps;
}
private static final ClassValue<DynamicComparators> CACHE
=new ClassValue<DynamicComparators>() {
@Override protected DynamicComparators computeValue(Class<?> type) {
return new DynamicComparators<>(type);
}
};
private final Class<T> theClass;
private final Map<String, Comparator<T>> comps;
private DynamicComparators(Class<T> cl) {
theClass=cl;
Map<String,Comparator<T>> map=new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
BeanInfo bi=Introspector.getBeanInfo(cl);
MethodHandles.Lookup l=MethodHandles.lookup();
MethodType invoked=MethodType.methodType(Function.class);
for(PropertyDescriptor pd: bi.getPropertyDescriptors()) {
Method m=pd.getReadMethod();
if(m==null) continue;
Class<?> t=m.getReturnType();
if(!t.isPrimitive() && !Comparable.class.isAssignableFrom(t))
continue;
MethodHandle mh=l.unreflect(m);
MethodType mt=mh.type();
@SuppressWarnings("unchecked")Comparator<T> cmp
= Comparator.comparing((Function<T,Comparable>)LambdaMetafactory
.metafactory(l, "apply", invoked, mt.generic(), mh, mt)
.getTarget().invokeExact());
map.put(pd.getName(), cmp);
}
} catch(Throwable ex) {
throw new RuntimeException(ex);
}
this.comps=Collections.unmodifiableMap(map);
}
@SuppressWarnings("unchecked") <U> DynamicComparators<U> cast(Class<U> cl) {
if(cl!=theClass) throw new ClassCastException();
return (DynamicComparators<U>)this;
}
}
Upvotes: 2
Reputation: 19702
You'll probably need something more dynamic. Some annotations may help
class Shoe
@Column("id")
@Sortable
public int getId(){ ... }
@Column("Description")
public String getDescription(){...}
Given any class, you can reflect on columns to display, columns that can be sorted ("id", ...), and values of columns ("getId()"
, ...).
Upvotes: 2
Reputation: 137299
A simple way, without having to rely on reflection magic, is to introduce a common interface for all the types having the same fields with the same datatypes as Entity
.
Consider the following IdSize
interface with the following Entity
.
interface IdSize {
Integer getId();
BigDecimal getSize();
}
class Entity implements IdSize {
private Integer id;
private BigDecimal size;
@Override
public Integer getId() {
return id;
}
@Override
public BigDecimal getSize() {
return size;
}
}
Then you can make your method generic like this:
public <T extends IdSize> List<Comparator<T>> sort(Map<String, String> map) {
List<Comparator<T>> list = new ArrayList<Comparator<T>>();
for (Map.Entry<String, String> entry : map.entrySet()) {
boolean sortOrder = entry.getValue().equalsIgnoreCase("asc");
Comparator<T> comparator = null;
switch (entry.getKey()) {
case "id":
comparator = Comparator.comparing(IdSize::getId);
break;
case "size":
comparator = Comparator.comparing(IdSize::getSize);
break;
default: // do something here, throw an exception?
}
list.add(sortOrder ? comparator : comparator.reversed());
}
return list;
}
(I refactored a little the switch-case statement to remove the duplicated code.). Also, you might want to add a default clause.
Upvotes: 4
Reputation: 53859
Use interfaces:
public interface Sizable {
BigDecimal getSize();
}
public interface Id {
int getId();
}
Have your classes implement those interface and use them in your generic methods:
public <T extends Id & Sizable> List<Comparator<T>> sort(Map<String, String> map) {
// ...
}
Upvotes: 3