Skip to content

Commit 067d4e4

Browse files
ssandodarrachequesne
authored andcommitted
[feat] Add the ability to filter on NULL values (darrachequesne#44)
1 parent 60b0141 commit 067d4e4

File tree

8 files changed

+225
-41
lines changed

8 files changed

+225
-41
lines changed

src/main/java/org/springframework/data/jpa/datatables/qrepository/PredicateFactory.java

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.springframework.data.jpa.datatables.qrepository;
22

3+
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.ESCAPED_NULL;
34
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.ESCAPED_OR_SEPARATOR;
5+
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.NULL;
46
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.OR_SEPARATOR;
57
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.isBoolean;
68

@@ -34,26 +36,52 @@ public static Predicate createPredicate(PathBuilder<?> entity, DataTablesInput i
3436
}
3537
if (filterValue.contains(OR_SEPARATOR)) {
3638
// the filter contains multiple values, add a 'WHERE .. IN' clause
37-
String[] values = filterValue.split(ESCAPED_OR_SEPARATOR);
38-
if (values.length > 0 && isBoolean(values[0])) {
39-
List<Boolean> booleanValues = new ArrayList<Boolean>();
40-
for (int i = 0; i < values.length; i++) {
41-
booleanValues.add(Boolean.valueOf(values[i]));
39+
boolean nullable = false;
40+
List<String> values = new ArrayList<String>();
41+
for (String value : filterValue.split(ESCAPED_OR_SEPARATOR)) {
42+
if (NULL.equals(value)) {
43+
nullable = true;
44+
} else {
45+
values.add(ESCAPED_NULL.equals(value) ? NULL : value); // to match a 'NULL' string
4246
}
43-
predicate = predicate.and(entity.getBoolean(column.getData()).in(booleanValues));
44-
} else {
45-
predicate.and(getStringExpression(entity, column.getData()).in(values));
4647
}
47-
} else {
48-
// the filter contains only one value, add a 'WHERE .. LIKE' clause
49-
if (isBoolean(filterValue)) {
50-
predicate =
51-
predicate.and(entity.getBoolean(column.getData()).eq(Boolean.valueOf(filterValue)));
48+
if (values.size() > 0 && isBoolean(values.get(0))) {
49+
List<Boolean> booleanValues = new ArrayList<Boolean>();
50+
for (int i = 0; i < values.size(); i++) {
51+
booleanValues.add(Boolean.valueOf(values.get(i)));
52+
}
53+
Predicate in = entity.getBoolean(column.getData()).in(booleanValues);
54+
if (nullable) {
55+
predicate = predicate.and(entity.getBoolean(column.getData()).isNull().or(in));
56+
} else {
57+
predicate = predicate.and(in);
58+
}
5259
} else {
53-
predicate = predicate.and(getStringExpression(entity, column.getData()).lower()
54-
.like(getLikeFilterValue(filterValue), ESCAPE_CHAR));
60+
Predicate in = getStringExpression(entity, column.getData()).in(values);
61+
if (nullable) {
62+
predicate = predicate.and(entity.get(column.getData()).isNull().or(in));
63+
} else {
64+
predicate = predicate.and(in);
65+
}
5566
}
67+
continue;
5668
}
69+
// the filter contains only one value, add a 'WHERE .. LIKE' clause
70+
if (isBoolean(filterValue)) {
71+
predicate =
72+
predicate.and(entity.getBoolean(column.getData()).eq(Boolean.valueOf(filterValue)));
73+
continue;
74+
}
75+
76+
StringExpression stringExpression = getStringExpression(entity, column.getData());
77+
if (NULL.equals(filterValue)) {
78+
predicate = predicate.and(stringExpression.isNull());
79+
continue;
80+
}
81+
82+
String likeFilterValue =
83+
getLikeFilterValue(ESCAPED_NULL.equals(filterValue) ? NULL : filterValue);
84+
predicate = predicate.and(stringExpression.lower().like(likeFilterValue, ESCAPE_CHAR));
5785
}
5886

5987
// check whether a global filter value exists

src/main/java/org/springframework/data/jpa/datatables/repository/DataTablesUtils.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public class DataTablesUtils {
1717
public final static String ATTRIBUTE_SEPARATOR = ".";
1818
public final static String ESCAPED_ATTRIBUTE_SEPARATOR = "\\.";
1919
public final static char ESCAPE_CHAR = '\\';
20+
public final static String NULL = "NULL";
21+
public final static String ESCAPED_NULL = "\\NULL";
2022

2123
/**
2224
* Creates a 'LIMIT .. OFFSET .. ORDER BY ..' clause for the given {@link DataTablesInput}.

src/main/java/org/springframework/data/jpa/datatables/repository/SpecificationFactory.java

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.ATTRIBUTE_SEPARATOR;
44
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.ESCAPED_ATTRIBUTE_SEPARATOR;
5+
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.ESCAPED_NULL;
56
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.ESCAPED_OR_SEPARATOR;
67
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.ESCAPE_CHAR;
8+
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.NULL;
79
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.OR_SEPARATOR;
810
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.getLikeFilterValue;
911
import static org.springframework.data.jpa.datatables.repository.DataTablesUtils.isBoolean;
1012

11-
import java.util.Arrays;
13+
import java.util.ArrayList;
14+
import java.util.List;
1215

1316
import javax.persistence.criteria.CriteriaBuilder;
1417
import javax.persistence.criteria.CriteriaQuery;
@@ -54,30 +57,55 @@ public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuild
5457

5558
if (filterValue.contains(OR_SEPARATOR)) {
5659
// the filter contains multiple values, add a 'WHERE .. IN' clause
57-
String[] values = filterValue.split(ESCAPED_OR_SEPARATOR);
58-
if (values.length > 0 && isBoolean(values[0])) {
59-
Object[] booleanValues = new Boolean[values.length];
60-
for (int i = 0; i < values.length; i++) {
61-
booleanValues[i] = Boolean.valueOf(values[i]);
60+
boolean nullable = false;
61+
List<String> values = new ArrayList<String>();
62+
for (String value : filterValue.split(ESCAPED_OR_SEPARATOR)) {
63+
if (NULL.equals(value)) {
64+
nullable = true;
65+
} else {
66+
values.add(ESCAPED_NULL.equals(value) ? NULL : value); // to match a 'NULL' string
6267
}
63-
booleanExpression = getExpression(root, column.getData(), Boolean.class);
64-
predicate = cb.and(predicate, booleanExpression.in(booleanValues));
65-
} else {
66-
stringExpression = getExpression(root, column.getData(), String.class);
67-
predicate = cb.and(predicate, stringExpression.in(Arrays.asList(values)));
6868
}
69-
} else {
70-
// the filter contains only one value, add a 'WHERE .. LIKE' clause
71-
if (isBoolean(filterValue)) {
69+
if (values.size() > 0 && isBoolean(values.get(0))) {
70+
Object[] booleanValues = new Boolean[values.size()];
71+
for (int i = 0; i < values.size(); i++) {
72+
booleanValues[i] = Boolean.valueOf(values.get(i));
73+
}
7274
booleanExpression = getExpression(root, column.getData(), Boolean.class);
73-
predicate =
74-
cb.and(predicate, cb.equal(booleanExpression, Boolean.valueOf(filterValue)));
75+
Predicate in = booleanExpression.in(booleanValues);
76+
if (nullable) {
77+
predicate = cb.and(predicate, cb.or(in, booleanExpression.isNull()));
78+
} else {
79+
predicate = cb.and(predicate, in);
80+
}
7581
} else {
7682
stringExpression = getExpression(root, column.getData(), String.class);
77-
predicate = cb.and(predicate,
78-
cb.like(cb.lower(stringExpression), getLikeFilterValue(filterValue), ESCAPE_CHAR));
83+
Predicate in = stringExpression.in(values);
84+
if (nullable) {
85+
predicate = cb.and(predicate, cb.or(in, stringExpression.isNull()));
86+
} else {
87+
predicate = cb.and(predicate, in);
88+
}
7989
}
90+
continue;
8091
}
92+
// the filter contains only one value, add a 'WHERE .. LIKE' clause
93+
if (isBoolean(filterValue)) {
94+
booleanExpression = getExpression(root, column.getData(), Boolean.class);
95+
predicate = cb.and(predicate, cb.equal(booleanExpression, Boolean.valueOf(filterValue)));
96+
continue;
97+
}
98+
99+
stringExpression = getExpression(root, column.getData(), String.class);
100+
if (NULL.equals(filterValue)) {
101+
predicate = cb.and(predicate, stringExpression.isNull());
102+
continue;
103+
}
104+
105+
String likeFilterValue =
106+
getLikeFilterValue(ESCAPED_NULL.equals(filterValue) ? NULL : filterValue);
107+
predicate =
108+
cb.and(predicate, cb.like(cb.lower(stringExpression), likeFilterValue, ESCAPE_CHAR));
81109
}
82110

83111
// check whether a global filter value exists

src/test/java/org/springframework/data/jpa/datatables/qrepository/QBillRepositoryTest.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ public void testWithoutFilter() {
2929
DataTablesOutput<Bill> output = billRepository.findAll(input);
3030
assertNotNull(output);
3131
assertNull(output.getError());
32-
assertEquals(12, output.getRecordsFiltered());
33-
assertEquals(12, output.getRecordsTotal());
32+
assertEquals(13, output.getRecordsFiltered());
33+
assertEquals(13, output.getRecordsTotal());
3434
}
3535

3636
@Test
@@ -44,6 +44,28 @@ public void testBooleanFilter() {
4444
assertEquals(6, output.getRecordsFiltered());
4545
}
4646

47+
@Test
48+
public void testBooleanFilterAndNull() {
49+
DataTablesInput input = getBasicInput();
50+
51+
input.getColumn("hasBeenPayed").setSearchValue("TRUE+NULL");
52+
DataTablesOutput<Bill> output = billRepository.findAll(input);
53+
assertNotNull(output);
54+
assertNull(output.getError());
55+
assertEquals(7, output.getRecordsFiltered());
56+
}
57+
58+
@Test
59+
public void testFilterIsNull() {
60+
DataTablesInput input = getBasicInput();
61+
62+
input.getColumn("hasBeenPayed").setSearchValue("NULL");
63+
DataTablesOutput<Bill> output = billRepository.findAll(input);
64+
assertNotNull(output);
65+
assertNull(output.getError());
66+
assertEquals(1, output.getRecordsFiltered());
67+
}
68+
4769
@Test
4870
public void testBooleanFilter2() {
4971
DataTablesInput input = getBasicInput();

src/test/java/org/springframework/data/jpa/datatables/qrepository/QUserRepositoryTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,46 @@ public void testFilterOnSeveralColumns() {
176176
assertEquals("ACTIVE", lastUser.getStatus().toString());
177177
}
178178

179+
@Test
180+
@Ignore
181+
public void testNullColumnFilter() {
182+
DataTablesInput input = getBasicInput();
183+
input.getColumn("home.town").setSearchValue("town0+NULL");
184+
185+
DataTablesOutput<User> output = userRepository.findAll(input);
186+
assertNotNull(output);
187+
assertEquals(1, output.getDraw());
188+
assertNull(output.getError());
189+
assertEquals(10, output.getRecordsFiltered());
190+
assertEquals(24, output.getRecordsTotal());
191+
}
192+
193+
@Test
194+
public void testEscapedOrNull() {
195+
DataTablesInput input = getBasicInput();
196+
input.getColumn("home.town").setSearchValue("town0+\\NULL");
197+
198+
DataTablesOutput<User> output = userRepository.findAll(input);
199+
assertNotNull(output);
200+
assertEquals(1, output.getDraw());
201+
assertNull(output.getError());
202+
assertEquals(6, output.getRecordsFiltered());
203+
assertEquals(24, output.getRecordsTotal());
204+
}
205+
206+
@Test
207+
public void testEscapedNull() {
208+
DataTablesInput input = getBasicInput();
209+
input.getColumn("home.town").setSearchValue("\\NULL");
210+
211+
DataTablesOutput<User> output = userRepository.findAll(input);
212+
assertNotNull(output);
213+
assertEquals(1, output.getDraw());
214+
assertNull(output.getError());
215+
assertEquals(1, output.getRecordsFiltered());
216+
assertEquals(24, output.getRecordsTotal());
217+
}
218+
179219
@Test
180220
public void testMultiFilterOnSameColumn() {
181221
DataTablesInput input = getBasicInput();

src/test/java/org/springframework/data/jpa/datatables/repository/BillRepositoryTest.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ public void testWithoutFilter() {
3030
DataTablesOutput<Bill> output = billRepository.findAll(input);
3131
assertNotNull(output);
3232
assertNull(output.getError());
33-
assertEquals(12, output.getRecordsFiltered());
34-
assertEquals(12, output.getRecordsTotal());
33+
assertEquals(13, output.getRecordsFiltered());
34+
assertEquals(13, output.getRecordsTotal());
3535
}
3636

3737
@Test
@@ -45,6 +45,28 @@ public void testBooleanFilter() {
4545
assertEquals(6, output.getRecordsFiltered());
4646
}
4747

48+
@Test
49+
public void testBooleanFilterAndNull() {
50+
DataTablesInput input = getBasicInput();
51+
52+
input.getColumn("hasBeenPayed").setSearchValue("TRUE+NULL");
53+
DataTablesOutput<Bill> output = billRepository.findAll(input);
54+
assertNotNull(output);
55+
assertNull(output.getError());
56+
assertEquals(7, output.getRecordsFiltered());
57+
}
58+
59+
@Test
60+
public void testFilterIsNull() {
61+
DataTablesInput input = getBasicInput();
62+
63+
input.getColumn("hasBeenPayed").setSearchValue("NULL");
64+
DataTablesOutput<Bill> output = billRepository.findAll(input);
65+
assertNotNull(output);
66+
assertNull(output.getError());
67+
assertEquals(1, output.getRecordsFiltered());
68+
}
69+
4870
@Test
4971
public void testBooleanFilter2() {
5072
DataTablesInput input = getBasicInput();

src/test/java/org/springframework/data/jpa/datatables/repository/UserRepositoryTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,45 @@ public void testFilterOnSeveralColumns() {
174174
assertEquals("ACTIVE", lastUser.getStatus().toString());
175175
}
176176

177+
@Test
178+
public void testNullColumnFilter() {
179+
DataTablesInput input = getBasicInput();
180+
input.getColumn("home.town").setSearchValue("town0+NULL");
181+
182+
DataTablesOutput<User> output = userRepository.findAll(input);
183+
assertNotNull(output);
184+
assertEquals(1, output.getDraw());
185+
assertNull(output.getError());
186+
assertEquals(10, output.getRecordsFiltered());
187+
assertEquals(24, output.getRecordsTotal());
188+
}
189+
190+
@Test
191+
public void testEscapedOrNull() {
192+
DataTablesInput input = getBasicInput();
193+
input.getColumn("home.town").setSearchValue("town0+\\NULL");
194+
195+
DataTablesOutput<User> output = userRepository.findAll(input);
196+
assertNotNull(output);
197+
assertEquals(1, output.getDraw());
198+
assertNull(output.getError());
199+
assertEquals(6, output.getRecordsFiltered());
200+
assertEquals(24, output.getRecordsTotal());
201+
}
202+
203+
@Test
204+
public void testEscapedNull() {
205+
DataTablesInput input = getBasicInput();
206+
input.getColumn("home.town").setSearchValue("\\NULL");
207+
208+
DataTablesOutput<User> output = userRepository.findAll(input);
209+
assertNotNull(output);
210+
assertEquals(1, output.getDraw());
211+
assertNull(output.getError());
212+
assertEquals(1, output.getRecordsFiltered());
213+
assertEquals(24, output.getRecordsTotal());
214+
}
215+
177216
@Test
178217
public void testMultiFilterOnSameColumn() {
179218
DataTablesInput input = getBasicInput();

src/test/resources/init.sql

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ INSERT INTO bill (id, description, amount, hasBeenPayed) VALUES
3030
(9, 'foo9', 900, false),
3131
(10, 'foo10', 1000, true),
3232
(11, 'foo11', 1100, false),
33-
(12, 'foo12', 1200, true);
33+
(12, 'foo12', 1200, true),
34+
(13, 'foo13', 1300, NULL);
3435

3536
INSERT INTO game (id, prize_name) VALUES
3637
(1, 'prize0'),
@@ -50,7 +51,9 @@ INSERT INTO home (id, town) VALUES
5051
(1, 'town0'),
5152
(2, 'town1'),
5253
(3, 'town2'),
53-
(4, 'town3');
54+
(4, 'town3'),
55+
(5, NULL),
56+
(6, 'NULL');
5457

5558
INSERT INTO users (id, username, role, status, id_home, visible) VALUES
5659
(1, 'john0', 'ADMIN', 'ACTIVE', null, true),
@@ -75,5 +78,5 @@ INSERT INTO users (id, username, role, status, id_home, visible) VALUES
7578
(20, 'john19', 'AUTHOR', 'BLOCKED', 4, false),
7679
(21, 'john20', 'USER', 'ACTIVE', 1, true),
7780
(22, 'john21', 'ADMIN', 'BLOCKED', 2, false),
78-
(23, 'john22', 'AUTHOR', 'ACTIVE', 3, true),
79-
(24, 'john23', 'USER', 'BLOCKED', 4, false);
81+
(23, 'john22', 'AUTHOR', 'ACTIVE', 6, true),
82+
(24, 'john23', 'USER', 'BLOCKED', 5, false);

0 commit comments

Comments
 (0)