Skip to content

Commit fcb1f01

Browse files
authored
Merge pull request #13825 from SORMAS-Foundation/bug/13428-disease-config-table-sorting
FIX: 13428: Sorting DiseaseConfiguration.
2 parents f4fc55e + 36107bc commit fcb1f01

6 files changed

Lines changed: 209 additions & 9 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package de.symeda.sormas.backend.common.sort;
2+
3+
import java.util.Arrays;
4+
import java.util.Map;
5+
import java.util.function.BiFunction;
6+
import java.util.function.Function;
7+
import java.util.stream.Collectors;
8+
9+
import javax.persistence.criteria.CriteriaBuilder;
10+
import javax.persistence.criteria.Expression;
11+
import javax.persistence.criteria.Root;
12+
13+
import org.jetbrains.annotations.NotNull;
14+
15+
/**
16+
* Utility class to be able to create sorting criterias on entities without using switch cases.
17+
*
18+
* Example:
19+
*
20+
* public static final Map<String, BiFunction<CriteriaBuilder, Root<DiseaseConfiguration>, Expression<?>>> SORTABLE_FIELDS_DICTIONARY =
21+
* EntitySortUtils.defaultSort(
22+
* DiseaseConfiguration.DISEASE,
23+
* DiseaseConfiguration.ACTIVE,
24+
* DiseaseConfiguration.PRIMARY_DISEASE,
25+
* DiseaseConfiguration.CASE_SURVEILLANCE_ENABLED,
26+
* DiseaseConfiguration.AGGREGATE_REPORTING_ENABLED,
27+
* DiseaseConfiguration.FOLLOW_UP_ENABLED,
28+
* DiseaseConfiguration.FOLLOW_UP_DURATION,
29+
* DiseaseConfiguration.CASE_FOLLOW_UP_DURATION,
30+
* DiseaseConfiguration.EVENT_PARTICIPANT_FOLLOW_UP_DURATION,
31+
* DiseaseConfiguration.EXTENDED_CLASSIFICATION,
32+
* DiseaseConfiguration.EXTENDED_CLASSIFICATION_MULTI,
33+
* DiseaseConfiguration.AUTOMATIC_SAMPLE_ASSIGNMENT_THRESHOLD);
34+
*/
35+
public class EntitySortUtils {
36+
37+
private EntitySortUtils() {
38+
}
39+
40+
/**
41+
* Will use the field name itself for sorting without any transformation.
42+
*
43+
* @param fieldNames
44+
* name of the filterable fields (must be exhaustive)
45+
* @return dictionary with key being a field and value being a bifunction returning the value without transformations.
46+
* @param <T>
47+
* Type information is important for Hibernate to prepare the adequate statements
48+
*/
49+
public static <T> Map<String, BiFunction<CriteriaBuilder, Root<T>, Expression<?>>> defaultSort(String... fieldNames) {
50+
return Arrays.stream(fieldNames).collect(Collectors.toMap(fieldName -> fieldName, defaultSort()));
51+
}
52+
53+
/**
54+
* Will use the field name itself for sorting by returning the lowercase values of the fields.
55+
*
56+
* @param fieldNames
57+
* name of the filterable fields (must be exhaustive)
58+
* @return dictionary with key being a field and value being a bifunction returning the lower case value.
59+
* @param <T>
60+
* Type information is important for Hibernate to prepare the adequate statements
61+
*/
62+
public static <T> Map<String, BiFunction<CriteriaBuilder, Root<T>, Expression<?>>> lowerCaseSort(String... fieldNames) {
63+
return Arrays.stream(fieldNames).collect(Collectors.toMap(fieldName -> fieldName, lowerCaseSort()));
64+
}
65+
66+
public static @NotNull <T> Function<String, BiFunction<CriteriaBuilder, Root<T>, Expression<?>>> defaultSort() {
67+
return fieldName -> (criteriaBuilder, root) -> root.get(fieldName);
68+
}
69+
70+
public static @NotNull <T> Function<String, BiFunction<CriteriaBuilder, Root<T>, Expression<?>>> lowerCaseSort() {
71+
return fieldName -> (criteriaBuilder, root) -> criteriaBuilder.lower(root.get(fieldName));
72+
}
73+
}

sormas-backend/src/main/java/de/symeda/sormas/backend/disease/DiseaseConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ public class DiseaseConfiguration extends AbstractDomainObject {
2727
public static final String PRIMARY_DISEASE = "primaryDisease";
2828
public static final String CASE_SURVEILLANCE_ENABLED = "caseSurveillanceEnabled";
2929
public static final String AGGREGATE_REPORTING_ENABLED = "aggregateReportingEnabled";
30+
public static final String FOLLOW_UP_ENABLED = "followUpEnabled";
31+
public static final String FOLLOW_UP_DURATION = "followUpDuration";
32+
public static final String CASE_FOLLOW_UP_DURATION = "caseFollowUpDuration";
33+
public static final String EVENT_PARTICIPANT_FOLLOW_UP_DURATION = "eventParticipantFollowUpDuration";
34+
public static final String EXTENDED_CLASSIFICATION = "extendedClassification";
35+
public static final String EXTENDED_CLASSIFICATION_MULTI = "extendedClassificationMulti";
36+
public static final String AUTOMATIC_SAMPLE_ASSIGNMENT_THRESHOLD = "automaticSampleAssignmentThreshold";
3037

3138
private Disease disease;
3239
private Boolean active;

sormas-backend/src/main/java/de/symeda/sormas/backend/disease/DiseaseConfigurationFacadeEjb.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
import java.util.EnumSet;
2626
import java.util.List;
2727
import java.util.Map;
28+
import java.util.Optional;
2829
import java.util.Set;
30+
import java.util.function.BiFunction;
2931
import java.util.stream.Collectors;
3032

3133
import javax.annotation.PostConstruct;
@@ -37,6 +39,7 @@
3739
import javax.ejb.Stateless;
3840
import javax.persistence.criteria.CriteriaBuilder;
3941
import javax.persistence.criteria.CriteriaQuery;
42+
import javax.persistence.criteria.Expression;
4043
import javax.persistence.criteria.Predicate;
4144
import javax.persistence.criteria.Root;
4245
import javax.validation.constraints.NotNull;
@@ -53,6 +56,7 @@
5356
import de.symeda.sormas.api.disease.DiseaseConfigurationIndexDto;
5457
import de.symeda.sormas.api.user.UserRight;
5558
import de.symeda.sormas.api.utils.SortProperty;
59+
import de.symeda.sormas.backend.common.sort.EntitySortUtils;
5660
import de.symeda.sormas.backend.user.User;
5761
import de.symeda.sormas.backend.user.UserService;
5862
import de.symeda.sormas.backend.util.DtoHelper;
@@ -61,6 +65,21 @@
6165
@Stateless(name = "DiseaseConfigurationFacade")
6266
public class DiseaseConfigurationFacadeEjb implements DiseaseConfigurationFacade {
6367

68+
public static final Map<String, BiFunction<CriteriaBuilder, Root<DiseaseConfiguration>, Expression<?>>> SORTABLE_FIELDS_DICTIONARY =
69+
EntitySortUtils.defaultSort(
70+
DiseaseConfiguration.DISEASE,
71+
DiseaseConfiguration.ACTIVE,
72+
DiseaseConfiguration.PRIMARY_DISEASE,
73+
DiseaseConfiguration.CASE_SURVEILLANCE_ENABLED,
74+
DiseaseConfiguration.AGGREGATE_REPORTING_ENABLED,
75+
DiseaseConfiguration.FOLLOW_UP_ENABLED,
76+
DiseaseConfiguration.FOLLOW_UP_DURATION,
77+
DiseaseConfiguration.CASE_FOLLOW_UP_DURATION,
78+
DiseaseConfiguration.EVENT_PARTICIPANT_FOLLOW_UP_DURATION,
79+
DiseaseConfiguration.EXTENDED_CLASSIFICATION,
80+
DiseaseConfiguration.EXTENDED_CLASSIFICATION_MULTI,
81+
DiseaseConfiguration.AUTOMATIC_SAMPLE_ASSIGNMENT_THRESHOLD);
82+
6483
protected final Logger logger = LoggerFactory.getLogger(getClass());
6584

6685
@EJB
@@ -135,16 +154,26 @@ public List<DiseaseConfigurationIndexDto> getIndexList(
135154
cq.where(filter);
136155
}
137156

138-
cq.orderBy(cb.asc(root.get(DiseaseConfiguration.DISEASE)), cb.asc(root.get(DiseaseConfiguration.ACTIVE)));
157+
if (CollectionUtils.isNotEmpty(sortProperties)) {
158+
cq.orderBy(sortProperties.stream().map(sortProperty -> {
159+
String sortPropertyName = sortProperty.propertyName;
160+
Expression<?> sortExpression = Optional.ofNullable(SORTABLE_FIELDS_DICTIONARY.get(sortPropertyName))
161+
.map(fct -> fct.apply(cb, root))
162+
.orElseThrow(
163+
() -> new IllegalArgumentException(
164+
String.format(
165+
"Invalid sort property: [%s] for [%s]. If must be sorted, then must be added into SORTABLE_FIELDS_DICTIONARY ",
166+
sortPropertyName,
167+
DiseaseConfiguration.class.getSimpleName())));
168+
return sortProperty.ascending ? cb.asc(sortExpression) : cb.desc(sortExpression);
169+
}).collect(Collectors.toList()));
170+
} else {
171+
cq.orderBy(cb.asc(root.get(DiseaseConfiguration.DISEASE)), cb.asc(root.get(DiseaseConfiguration.ACTIVE)));
172+
}
139173

140174
cq.select(root);
141175

142-
List<DiseaseConfigurationIndexDto> resultList = getResultList(service.getEntityManager(), cq, first, max, this::toIndexDto);
143-
for (DiseaseConfigurationIndexDto dto : resultList) {
144-
145-
}
146-
147-
return resultList;
176+
return getResultList(service.getEntityManager(), cq, first, max, this::toIndexDto);
148177
}
149178

150179
private DiseaseConfigurationIndexDto toIndexDto(DiseaseConfiguration entity) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package de.symeda.sormas.ui;
2+
3+
import java.util.Comparator;
4+
import java.util.function.Function;
5+
6+
import org.apache.commons.lang3.StringUtils;
7+
8+
/**
9+
* Warning by default Vaadin uses {@link Comparable#compareTo( Object)}
10+
* By default, enums are sorted using {@link Enum#ordinal()} which is a bad pick for "human-intuitive-sorting",
11+
* provides some utility methods to simplify this endeavor.
12+
* <p>
13+
* Could be further enhanced by using: Vaadin's SerializableComparator class, but not done to avoid additional vaadin-server dependency.
14+
*/
15+
public class EnumSortUtils {
16+
17+
private EnumSortUtils() {
18+
}
19+
20+
public static <T, U extends Enum<?>> Comparator<T> comparingOnEnumField(Function<? super T, ? extends U> keyExtractor) {
21+
return Comparator.nullsLast(Comparator.comparing(keyExtractor, enumNameComparator()));
22+
}
23+
24+
public static <T extends Enum<?>> Comparator<T> enumNameComparator() {
25+
return Comparator.nullsLast(Comparator.comparing(Enum::name, StringUtils::compare));
26+
}
27+
}

sormas-ui/src/main/java/de/symeda/sormas/ui/configuration/disease/DiseaseConfigurationGrid.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515

1616
package de.symeda.sormas.ui.configuration.disease;
1717

18+
import static de.symeda.sormas.ui.EnumSortUtils.comparingOnEnumField;
19+
20+
import java.util.List;
21+
1822
import de.symeda.sormas.api.FacadeProvider;
1923
import de.symeda.sormas.api.disease.DiseaseConfigurationCriteria;
2024
import de.symeda.sormas.api.disease.DiseaseConfigurationDto;
@@ -33,9 +37,12 @@ public DiseaseConfigurationGrid(DiseaseConfigurationCriteria criteria) {
3337
super(DiseaseConfigurationIndexDto.class);
3438
setSizeFull();
3539

36-
setLazyDataProvider(FacadeProvider.getDiseaseConfigurationFacade()::getIndexList, FacadeProvider.getDiseaseConfigurationFacade()::count);
40+
setInEagerMode(true);
41+
3742
setCriteria(criteria);
3843

44+
setEagerDataProvider();
45+
3946
setColumns(
4047
DiseaseConfigurationIndexDto.DISEASE,
4148
DiseaseConfigurationIndexDto.ACTIVE,
@@ -51,6 +58,8 @@ public DiseaseConfigurationGrid(DiseaseConfigurationCriteria criteria) {
5158
DiseaseConfigurationIndexDto.EXTENDED_CLASSIFICATION_MULTI,
5259
DiseaseConfigurationIndexDto.AUTOMATIC_SAMPLE_ASSIGNMENT_THRESHOLD);
5360

61+
getColumn(DiseaseConfigurationIndexDto.AGE_GROUPS).setSortable(false);
62+
5463
addEditColumn(e -> ControllerProvider.getDiseaseConfirgurationController().editDiseaseConfiguration(e.getUuid()));
5564

5665
for (Column<?, ?> column : getColumns()) {
@@ -64,9 +73,17 @@ public DiseaseConfigurationGrid(DiseaseConfigurationCriteria criteria) {
6473
getColumn(DiseaseConfigurationIndexDto.EXTENDED_CLASSIFICATION).setRenderer(new BooleanRenderer());
6574
getColumn(DiseaseConfigurationIndexDto.EXTENDED_CLASSIFICATION_MULTI).setRenderer(new BooleanRenderer());
6675
getColumn(DiseaseConfigurationIndexDto.AGGREGATE_REPORTING_ENABLED).setRenderer(new BooleanRenderer());
76+
77+
getColumn(DiseaseConfigurationIndexDto.DISEASE)
78+
.setComparator((o1, o2) -> comparingOnEnumField(DiseaseConfigurationIndexDto::getDisease).compare(o1, o2));
79+
80+
}
81+
82+
private void setEagerDataProvider() {
83+
setDataProvider(FacadeProvider.getDiseaseConfigurationFacade().getIndexList(getCriteria(), null, null, List.of()).stream());
6784
}
6885

6986
public void reload() {
70-
getDataProvider().refreshAll();
87+
setEagerDataProvider();
7188
}
7289
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package de.symeda.sormas.ui;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.Arrays;
6+
import java.util.Comparator;
7+
import java.util.List;
8+
import java.util.stream.Collectors;
9+
10+
import org.junit.jupiter.api.Test;
11+
12+
import de.symeda.sormas.api.Disease;
13+
14+
class EnumSortUtilsTest {
15+
16+
@Test
17+
void testComparingOnEnumField_sortsByEnumFieldName() {
18+
class DiseaseHolder {
19+
20+
final Disease disease;
21+
22+
DiseaseHolder(Disease d) {
23+
this.disease = d;
24+
}
25+
26+
Disease getDisease() {
27+
return disease;
28+
}
29+
}
30+
31+
List<DiseaseHolder> list = Arrays.asList(
32+
null,
33+
new DiseaseHolder(Disease.MEASLES),
34+
new DiseaseHolder(Disease.ANTHRAX),
35+
new DiseaseHolder(Disease.CORONAVIRUS),
36+
new DiseaseHolder(null));
37+
38+
Comparator<DiseaseHolder> comparator = EnumSortUtils.comparingOnEnumField(DiseaseHolder::getDisease);
39+
40+
List<DiseaseHolder> sorted = list.stream().sorted(comparator).collect(Collectors.toList());
41+
42+
List<Disease> resultOrder =
43+
sorted.stream().map(diseaseHolder -> diseaseHolder != null ? diseaseHolder.getDisease() : null).collect(Collectors.toList());
44+
45+
assertEquals(Arrays.asList(Disease.ANTHRAX, Disease.CORONAVIRUS, Disease.MEASLES, null, null), resultOrder);
46+
}
47+
}

0 commit comments

Comments
 (0)