Java, list sorting with reflection

Viewed 249

I want to allow to sort by every field in the class, without having to write switch/ if statements. My idea was to find the Field that matches given string value by name and then, with Stream API neatly sort. IntelliJ screamed that i need to surround it with try-catch, so it is not so neatly looking, but that's not important, as it does not work.

    private List<MyEntity> getSorted(List<MyEntity> list, SearchCriteria criteria) {
        Field sortByField = findFieldInListByName(getFieldList(MyEntity.class), criteria.getSortBy());
        return list.stream().sorted(Comparator.comparing(entity-> {
            try {
                return (MyEntity) sortByField.get(entity);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            return entity;
        })).collect(Collectors.toList());
    }

In the MyEntity class I have added Comparable interface, but I am not sure what should be in the body of Compare(), as I dont want to specify how to compare objects, because it will change based on the selected sorting.

EDIT: Added Entity below:

@Data
@Entity
@Table(name = "role_management", schema = "mdr")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode

public class MyEntity implements Comparable{

    @Id
    @Column(name = "uuid", unique = true, insertable = false, updatable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID uuid;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private UserEntity user;

    @Basic
    @NonNull
    @Column(name = "role")
    private String role;

    @Basic
    @Column(name = "action")
    @Enumerated(EnumType.STRING)
    private RoleAction action;

    @Basic
    @Column(name = "goal")
    @Enumerated(EnumType.STRING)
    private RoleGoal goal;

    @Column(name = "date")
    private LocalDateTime date;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "reporter_id", referencedColumnName = "uuid")
    private UserEntity reporter;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "authorizer_id", referencedColumnName = "uuid")
    private UserEntity authorizer;

    @Basic
    @Column(name = "ezd")
    private String ezd;

    @Basic
    @Column(name = "is_last")
    private boolean isMostRecent;

    @Override
    public int compareTo(Object o) {
        return 0;
    }
}

EDIT 2: My code based on the @Sweeper solution:

UserEntity (nullable)

    @Override
    public int compareTo(UserEntity other) {
        if (other == null) {
            return 1;
        }
        return this.getMail().compareTo(other.getMail());
    }

Comparator:

    public static Comparator getSortComparator(Field sortByField) {
        return Comparator.nullsLast(Comparator.comparing(entity -> {
            try {
                Object fieldValue = sortByField.get(entity);
                if (!(fieldValue instanceof Comparable<?>) && fieldValue != null) {
                    throw new IllegalArgumentException("...");
                }
                return (Comparable) fieldValue;
            } catch (IllegalAccessException e) {
                throw new MdrCommonException(e.getLocalizedMessage());
            }
        }));
    }
2 Answers

MyEntity should not implement Comparable. It is the fields, by which you are going to sort the list of MyEntity objects, that needs to be Comparable. For example, if you are sorting by the field user, which is a UserEntity, then UserEntity is the thing that needs to be comparable, not MyEntity.

The lambda's job should just be to check that the fields are indeed Comparable, and throw an exception if they are not.

Since you don't know the types of the fields at compile time, however, you'd have to use a raw type here. The comparing call would look like this:

Comparator.comparing(entity -> {
    try {
        Object fieldValue = sortByField.get(entity);

        // This check still passes if the type of fieldValue implements Comparable<U>, 
        // where U is an unrelated type from the type of fieldValue, but this is the
        // best we can do here, since we don't know the type of field at compile time
        if (!(fieldValue instanceof Comparable<?>) && fieldValue != null) {
            throw new IllegalArgumentException("Field is not comparable!");
        }
        return (Comparable)fieldValue;
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
})

You can create automatically comparators for any field of any class using reflection but is better create specific comparators (will be typechecked).

Your entity is a normal class with normal fields then, the usual Java sorting machinery should do the job:

Basically, if you define one comparator for every field (even deep fields into your entity):

public final static Comparator<MyEntity> ByField1 = comparing(MyEntity::getField1);
public final static Comparator<MyEntity> ByField2 = comparing(MyEntity::getField2);
public final static Comparator<MyEntity> ByField3 = comparing(MyEntity::getField3);
public final static Comparator<MyEntity> ByDeep1 = comparing(a -> a.getField4().getDeep1());
public final static Comparator<MyEntity> ByDeep2 = comparing(a -> a.getField4().getDeep2());
public final static Comparator<MyEntity> ByDeep3 = comparing(a -> a.getField4().getDeep3());

You can sort using complex sorting expressions:

data.stream()
        .sorted(ByField2.reversed().thenComparing(ByDeep2))
        .forEach(System.out::println);

a full example could be

public static void main(String[] args) {

    List<MyEntity> data =
            Stream.of("Row1", "Row2").flatMap(field1 ->
            Stream.of(101, 102).flatMap(field2 ->
            Stream.of(true, false).flatMap(field3 ->
            Stream.of("Row1", "Row2").flatMap(deep1 ->
            Stream.of(101, 102).flatMap(deep2 ->
            Stream.of(true, false).map(deep3 ->
                new MyEntity(field1, field2, field3, new MyDeepField(deep1, deep2, deep3))))))))
            .collect(toList());

    data.stream()
            .sorted(ByField2.reversed().thenComparing(ByDeep2))
            .forEach(System.out::println);

}

@Getter
@Setter
@AllArgsConstructor
static class MyDeepField {
    private String deep1;
    private Integer deep2;
    private Boolean deep3;
}

@Getter
@Setter
@AllArgsConstructor
static class MyEntity {
    private String field1;
    private Integer field2;
    private Boolean field3;
    private MyDeepField field4;

    public final static Comparator<MyEntity> ByField1 = comparing(MyEntity::getField1);
    public final static Comparator<MyEntity> ByField2 = comparing(MyEntity::getField2);
    public final static Comparator<MyEntity> ByField3 = comparing(MyEntity::getField3);
    public final static Comparator<MyEntity> ByDeep1 = comparing(a -> a.getField4().getDeep1());
    public final static Comparator<MyEntity> ByDeep2 = comparing(a -> a.getField4().getDeep2());
    public final static Comparator<MyEntity> ByDeep3 = comparing(a -> a.getField4().getDeep3());

    @Override
    public String toString() {
        return "MyEntity{" +
                "field1='" + field1 + '\'' +
                ", field2=" + field2 +
                ", field3=" + field3 +
                ", deep1=" + field4.getDeep1() +
                ", deep2=" + field4.getDeep2() +
                ", deep3=" + field4.getDeep3() +
                '}';
    }
}

with output

MyEntity{field1='Row1', field2=102, field3=true, deep1=Row1, deep2=101, deep3=true}
MyEntity{field1='Row1', field2=102, field3=true, deep1=Row1, deep2=101, deep3=false}
...
MyEntity{field1='Row2', field2=101, field3=false, deep1=Row2, deep2=102, deep3=true}
MyEntity{field1='Row2', field2=101, field3=false, deep1=Row2, deep2=102, deep3=false}

The criteria field into your SearchCriteria class is some field of type Comparator<MyEntity> or a mapping using an enumeration or parsing string expressions or so...

Related