Skip to content

Commit 16803f9

Browse files
feat: add support for the SearchPanes extension
Usage: ```java @RequestMapping(value = "/data/users", method = RequestMethod.GET) public DataTablesOutput<User> getUsers(@Valid DataTablesInput input, @RequestParam Map<String, String> queryParams) { input.parseSearchPanesFromQueryParams(queryParams, Arrays.asList("position", "status")); return userRepository.findAll(input); } ``` Related: #118
1 parent 6a0d37d commit 16803f9

File tree

10 files changed

+262
-10
lines changed

10 files changed

+262
-10
lines changed

README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class UserRestController {
3434
- [Getting started](#getting-started)
3535
- [1. Enable the use of `DataTablesRepository` factory](#1-enable-the-use-of-datatablesrepository-factory)
3636
- [2. Create a new entity](#2-create-a-new-entity)
37-
- [3. Extend the DataTablesRepository interface](3-extend-the-datatablesrepository-interface)
37+
- [3. Extend the DataTablesRepository interface](#3-extend-the-datatablesrepository-interface)
3838
- [4. On the client-side, create a new DataTable object](#4-on-the-client-side-create-a-new-datatable-object)
3939
- [5. Fix the serialization / deserialization of the query parameters](#5-fix-the-serialization--deserialization-of-the-query-parameters)
4040
- [API](#api)
@@ -43,6 +43,7 @@ public class UserRestController {
4343
- [Manage non-searchable fields](#manage-non-searchable-fields)
4444
- [Limit the exposed attributes of the entities](#limit-the-exposed-attributes-of-the-entities)
4545
- [Search on a rendered column](#search-on-a-rendered-column)
46+
- [Use with the SearchPanes extension](#use-with-the-searchpanes-extension)
4647
- [Troubleshooting](#troubleshooting)
4748

4849
## Maven dependency
@@ -537,6 +538,52 @@ You can find a complete example [here](https://github.com/darrachequesne/spring-
537538
538539
Back to [top](#spring-data-jpa-datatables).
539540
541+
### Use with the SearchPanes extension
542+
543+
Server-side:
544+
545+
```java
546+
@RestController
547+
@RequiredArgsConstructor
548+
public class UserRestController {
549+
private final UserRepository userRepository;
550+
551+
@RequestMapping(value = "/data/users", method = RequestMethod.GET)
552+
public DataTablesOutput<User> getUsers(@Valid DataTablesInput input, @RequestParam Map<String, String> queryParams) {
553+
input.parseSearchPanesFromQueryParams(queryParams, Arrays.asList("position", "status"));
554+
return userRepository.findAll(input);
555+
}
556+
}
557+
```
558+
559+
Client-side:
560+
561+
```js
562+
$(document).ready(function() {
563+
var table = $('table#sample').DataTable({
564+
ajax : '/data/users',
565+
serverSide: true,
566+
dom: 'Pfrtip',
567+
columns : [{
568+
data : 'id'
569+
}, {
570+
data : 'mail'
571+
}, {
572+
data : 'position'
573+
}, {
574+
data : 'status'
575+
}]
576+
});
577+
}
578+
```
579+
580+
Regarding the deserialization issue detailed [above](#5-fix-the-serialization--deserialization-of-the-query-parameters), here is the compatibility matrix:
581+
582+
| Solution | Compatibility with the SearchPanes extension |
583+
| --- | --- |
584+
| `jquery.spring-friendly.js` | YES |
585+
| POST requests | NO |
586+
| `flatten()` method | NO |
540587
541588
## Troubleshooting
542589

src/main/java/org/springframework/data/jpa/datatables/PredicateBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ public PredicateBuilder(PathBuilder<?> entity, DataTablesInput input) {
2222
public Predicate build() {
2323
initPredicatesRecursively(tree, entity);
2424

25+
if (input.getSearchPanes() != null) {
26+
input.getSearchPanes().forEach((attribute, values) -> {
27+
if (!values.isEmpty()) {
28+
columnPredicates.add(entity.get(attribute).in(values));
29+
}
30+
});
31+
}
32+
2533
return createFinalPredicate();
2634
}
2735

src/main/java/org/springframework/data/jpa/datatables/SpecificationBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ private class DataTablesSpecification<S> implements Specification<S> {
2727
public Predicate toPredicate(@NonNull Root<S> root, @NonNull CriteriaQuery<?> query, @NonNull CriteriaBuilder criteriaBuilder) {
2828
initPredicatesRecursively(tree, root, root, criteriaBuilder);
2929

30+
if (input.getSearchPanes() != null) {
31+
input.getSearchPanes().forEach((attribute, values) -> {
32+
if (!values.isEmpty()) {
33+
columnPredicates.add(root.get(attribute).in(values));
34+
}
35+
});
36+
}
37+
3038
boolean isCountQuery = query.getResultType() == Long.class;
3139
if (isCountQuery) {
3240
root.getFetches().clear();

src/main/java/org/springframework/data/jpa/datatables/mapping/DataTablesInput.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
import javax.validation.constraints.Min;
66
import javax.validation.constraints.NotEmpty;
77
import javax.validation.constraints.NotNull;
8-
import java.util.ArrayList;
9-
import java.util.HashMap;
10-
import java.util.List;
11-
import java.util.Map;
8+
import java.util.*;
9+
import java.util.regex.Matcher;
10+
import java.util.regex.Pattern;
1211

1312
@Data
1413
public class DataTablesInput {
14+
/**
15+
* Format: <code>searchPanes.$attribute.0</code> (<code>searchPanes[$attribute][0]</code> without jquery.spring-friendly.js)
16+
*
17+
* @see <a href="https://github.com/DataTables/SearchPanes/blob/5e6d3229cd90594cc67d6d266321f1c922fc9231/src/searchPanes.ts#L119-L137">source</a>
18+
*/
19+
private static final Pattern SEARCH_PANES_REGEX = Pattern.compile("^searchPanes\\.(\\w+)\\.\\d+$");
1520

1621
/**
1722
* Draw counter. This is used by DataTables to ensure that the Ajax returns from server-side
@@ -59,6 +64,11 @@ public class DataTablesInput {
5964
@NotEmpty
6065
private List<Column> columns = new ArrayList<>();
6166

67+
/**
68+
* Input for the <a href="https://datatables.net/extensions/searchpanes/">SearchPanes extension</a>
69+
*/
70+
private Map<String, Set<String>> searchPanes;
71+
6272
/**
6373
*
6474
* @return a {@link Map} of {@link Column} indexed by name
@@ -121,4 +131,21 @@ public void addOrder(String columnName, boolean ascending) {
121131
}
122132
}
123133

134+
public void parseSearchPanesFromQueryParams(Map<String, String> queryParams, Collection<String> attributes) {
135+
Map<String, Set<String>> searchPanes = new HashMap<>();
136+
attributes.forEach(attribute -> searchPanes.put(attribute, new HashSet<>()));
137+
138+
queryParams.forEach((key, value) -> {
139+
Matcher matcher = SEARCH_PANES_REGEX.matcher(key);
140+
if (matcher.matches()) {
141+
String attribute = matcher.group(1);
142+
if (attributes.contains(attribute)) {
143+
searchPanes.get(attribute).add(value);
144+
}
145+
}
146+
});
147+
148+
this.searchPanes = searchPanes;
149+
}
150+
124151
}

src/main/java/org/springframework/data/jpa/datatables/mapping/DataTablesOutput.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ public class DataTablesOutput<T> {
3939
@JsonView(View.class)
4040
private List<T> data = Collections.emptyList();
4141

42+
/**
43+
* Output for the <a href="https://datatables.net/extensions/searchpanes/">SearchPanes extension</a>
44+
*/
45+
@JsonView(View.class)
46+
private SearchPanes searchPanes;
47+
4248
/**
4349
* Optional: If an error occurs during the running of the server-side processing script, you can
4450
* inform the user of this error by passing back the error message to be displayed using this
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.springframework.data.jpa.datatables.mapping;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
@Data
10+
@AllArgsConstructor
11+
public class SearchPanes {
12+
private Map<String, List<Item>> options;
13+
14+
@Data
15+
@AllArgsConstructor
16+
public static class Item {
17+
private String label;
18+
private String value;
19+
private long total;
20+
private long count;
21+
}
22+
23+
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,29 @@
22

33
import com.querydsl.core.BooleanBuilder;
44
import com.querydsl.core.types.EntityPath;
5+
import com.querydsl.core.types.Ops;
56
import com.querydsl.core.types.Predicate;
67
import com.querydsl.core.types.dsl.PathBuilder;
78
import org.springframework.data.domain.Page;
89
import org.springframework.data.jpa.datatables.PredicateBuilder;
910
import org.springframework.data.jpa.datatables.mapping.DataTablesInput;
1011
import org.springframework.data.jpa.datatables.mapping.DataTablesOutput;
12+
import org.springframework.data.jpa.datatables.mapping.SearchPanes;
1113
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
1214
import org.springframework.data.jpa.repository.support.QuerydslJpaRepository;
1315
import org.springframework.data.querydsl.EntityPathResolver;
1416
import org.springframework.data.querydsl.SimpleEntityPathResolver;
1517

1618
import javax.persistence.EntityManager;
1719
import java.io.Serializable;
20+
import java.util.ArrayList;
21+
import java.util.HashMap;
1822
import java.util.List;
23+
import java.util.Map;
1924
import java.util.function.Function;
2025

26+
import static com.querydsl.core.types.dsl.Expressions.stringOperation;
27+
2128
public class QDataTablesRepositoryImpl<T, ID extends Serializable>
2229
extends QuerydslJpaRepository<T, ID> implements QDataTablesRepository<T, ID> {
2330

@@ -91,10 +98,38 @@ public <R> DataTablesOutput<R> findAll(DataTablesInput input, Predicate addition
9198
output.setData(content);
9299
output.setRecordsFiltered(data.getTotalElements());
93100

101+
if (input.getSearchPanes() != null) {
102+
output.setSearchPanes(computeSearchPanes(input, predicate));
103+
}
94104
} catch (Exception e) {
95105
output.setError(e.toString());
96106
}
97107

98108
return output;
99109
}
110+
111+
private SearchPanes computeSearchPanes(DataTablesInput input, Predicate predicate) {
112+
Map<String, List<SearchPanes.Item>> options = new HashMap<>();
113+
114+
input.getSearchPanes().forEach((attribute, values) -> {
115+
List<SearchPanes.Item> items = new ArrayList<>();
116+
PathBuilder<Object> path = this.builder.get(attribute);
117+
118+
this.createQuery()
119+
.select(stringOperation(Ops.STRING_CAST, path), path.count())
120+
.where(predicate)
121+
.groupBy(path)
122+
.fetchResults()
123+
.getResults()
124+
.forEach(tuple -> {
125+
String value = tuple.get(0, String.class);
126+
long count = tuple.get(1, Long.class);
127+
items.add(new SearchPanes.Item(value, value, count, count));
128+
});
129+
130+
options.put(attribute, items);
131+
});
132+
133+
return new SearchPanes(options);
134+
}
100135
}

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

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,31 @@
44
import org.springframework.data.jpa.datatables.SpecificationBuilder;
55
import org.springframework.data.jpa.datatables.mapping.DataTablesInput;
66
import org.springframework.data.jpa.datatables.mapping.DataTablesOutput;
7+
import org.springframework.data.jpa.datatables.mapping.SearchPanes;
78
import org.springframework.data.jpa.domain.Specification;
89
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
910
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
1011

1112
import javax.persistence.EntityManager;
13+
import javax.persistence.criteria.CriteriaBuilder;
14+
import javax.persistence.criteria.CriteriaQuery;
15+
import javax.persistence.criteria.Root;
1216
import java.io.Serializable;
17+
import java.util.ArrayList;
18+
import java.util.HashMap;
1319
import java.util.List;
20+
import java.util.Map;
1421
import java.util.function.Function;
1522

1623
public class DataTablesRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
1724
implements DataTablesRepository<T, ID> {
25+
private final EntityManager entityManager;
1826

1927
DataTablesRepositoryImpl(JpaEntityInformation<T, ?> entityInformation,
2028
EntityManager entityManager) {
2129

2230
super(entityInformation, entityManager);
31+
this.entityManager = entityManager;
2332
}
2433

2534
@Override
@@ -63,23 +72,51 @@ public <R> DataTablesOutput<R> findAll(DataTablesInput input,
6372
output.setRecordsTotal(recordsTotal);
6473

6574
SpecificationBuilder<T> specificationBuilder = new SpecificationBuilder<>(input);
66-
Page<T> data = findAll(
67-
Specification.where(specificationBuilder.build())
68-
.and(additionalSpecification)
69-
.and(preFilteringSpecification),
70-
specificationBuilder.createPageable());
75+
Specification<T> specification = Specification.where(specificationBuilder.build())
76+
.and(additionalSpecification)
77+
.and(preFilteringSpecification);
78+
Page<T> data = findAll(specification, specificationBuilder.createPageable());
7179

7280
@SuppressWarnings("unchecked")
7381
List<R> content =
7482
converter == null ? (List<R>) data.getContent() : data.map(converter).getContent();
7583
output.setData(content);
7684
output.setRecordsFiltered(data.getTotalElements());
7785

86+
if (input.getSearchPanes() != null) {
87+
output.setSearchPanes(computeSearchPanes(input, specification));
88+
}
7889
} catch (Exception e) {
7990
output.setError(e.toString());
8091
}
8192

8293
return output;
8394
}
8495

96+
private SearchPanes computeSearchPanes(DataTablesInput input, Specification<T> specification) {
97+
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
98+
Map<String, List<SearchPanes.Item>> options = new HashMap<>();
99+
100+
input.getSearchPanes().forEach((attribute, values) -> {
101+
CriteriaQuery<Object[]> query = criteriaBuilder.createQuery(Object[].class);
102+
Root<T> root = query.from(getDomainClass());
103+
query.multiselect(root.get(attribute), criteriaBuilder.count(root));
104+
query.groupBy(root.get(attribute));
105+
query.where(specification.toPredicate(root, query, criteriaBuilder));
106+
root.getFetches().clear();
107+
108+
List<SearchPanes.Item> items = new ArrayList<>();
109+
110+
this.entityManager.createQuery(query).getResultList().forEach(objects -> {
111+
String value = String.valueOf(objects[0]);
112+
long count = (long) objects[1];
113+
items.add(new SearchPanes.Item(value, value, count, count));
114+
});
115+
116+
options.put(attribute, items);
117+
});
118+
119+
return new SearchPanes(options);
120+
}
121+
85122
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.springframework.data.jpa.datatables.mapping;
2+
3+
import org.junit.Test;
4+
5+
import java.util.*;
6+
7+
import static java.util.Arrays.asList;
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.entry;
10+
11+
public class DataTablesInputTest {
12+
13+
@Test
14+
public void testParseSearchPanes() {
15+
DataTablesInput input = new DataTablesInput();
16+
HashMap<String, String> queryParams = new HashMap<>();
17+
queryParams.put("searchPanes.attr1.0", "1");
18+
queryParams.put("searchPanes.attr1.1", "2");
19+
queryParams.put("searchPanes.attr2.0", "3");
20+
queryParams.put("searchPanes.attr3.test", "4");
21+
queryParams.put("searchPanes.attr4.0", "5");
22+
queryParams.put("ignored", "6");
23+
24+
input.parseSearchPanesFromQueryParams(queryParams, asList("attr1", "attr2"));
25+
26+
assertThat(input.getSearchPanes()).containsOnly(
27+
entry("attr1", new HashSet<>(asList("1", "2"))),
28+
entry("attr2", new HashSet<>(asList("3")))
29+
);
30+
}
31+
}

0 commit comments

Comments
 (0)