Skip to content

Commit

Permalink
feat: add support for the SearchPanes extension
Browse files Browse the repository at this point in the history
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
  • Loading branch information
darrachequesne committed Mar 17, 2021
1 parent 6a0d37d commit 16803f9
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 10 deletions.
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class UserRestController {
- [Getting started](#getting-started)
- [1. Enable the use of `DataTablesRepository` factory](#1-enable-the-use-of-datatablesrepository-factory)
- [2. Create a new entity](#2-create-a-new-entity)
- [3. Extend the DataTablesRepository interface](3-extend-the-datatablesrepository-interface)
- [3. Extend the DataTablesRepository interface](#3-extend-the-datatablesrepository-interface)
- [4. On the client-side, create a new DataTable object](#4-on-the-client-side-create-a-new-datatable-object)
- [5. Fix the serialization / deserialization of the query parameters](#5-fix-the-serialization--deserialization-of-the-query-parameters)
- [API](#api)
Expand All @@ -43,6 +43,7 @@ public class UserRestController {
- [Manage non-searchable fields](#manage-non-searchable-fields)
- [Limit the exposed attributes of the entities](#limit-the-exposed-attributes-of-the-entities)
- [Search on a rendered column](#search-on-a-rendered-column)
- [Use with the SearchPanes extension](#use-with-the-searchpanes-extension)
- [Troubleshooting](#troubleshooting)

## Maven dependency
Expand Down Expand Up @@ -537,6 +538,52 @@ You can find a complete example [here](https://github.com/darrachequesne/spring-
Back to [top](#spring-data-jpa-datatables).
### Use with the SearchPanes extension
Server-side:
```java
@RestController
@RequiredArgsConstructor
public class UserRestController {
private final UserRepository userRepository;

@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);
}
}
```
Client-side:
```js
$(document).ready(function() {
var table = $('table#sample').DataTable({
ajax : '/data/users',
serverSide: true,
dom: 'Pfrtip',
columns : [{
data : 'id'
}, {
data : 'mail'
}, {
data : 'position'
}, {
data : 'status'
}]
});
}
```
Regarding the deserialization issue detailed [above](#5-fix-the-serialization--deserialization-of-the-query-parameters), here is the compatibility matrix:
| Solution | Compatibility with the SearchPanes extension |
| --- | --- |
| `jquery.spring-friendly.js` | YES |
| POST requests | NO |
| `flatten()` method | NO |
## Troubleshooting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ public PredicateBuilder(PathBuilder<?> entity, DataTablesInput input) {
public Predicate build() {
initPredicatesRecursively(tree, entity);

if (input.getSearchPanes() != null) {
input.getSearchPanes().forEach((attribute, values) -> {
if (!values.isEmpty()) {
columnPredicates.add(entity.get(attribute).in(values));
}
});
}

return createFinalPredicate();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ private class DataTablesSpecification<S> implements Specification<S> {
public Predicate toPredicate(@NonNull Root<S> root, @NonNull CriteriaQuery<?> query, @NonNull CriteriaBuilder criteriaBuilder) {
initPredicatesRecursively(tree, root, root, criteriaBuilder);

if (input.getSearchPanes() != null) {
input.getSearchPanes().forEach((attribute, values) -> {
if (!values.isEmpty()) {
columnPredicates.add(root.get(attribute).in(values));
}
});
}

boolean isCountQuery = query.getResultType() == Long.class;
if (isCountQuery) {
root.getFetches().clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

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

/**
* Input for the <a href="https://datatables.net/extensions/searchpanes/">SearchPanes extension</a>
*/
private Map<String, Set<String>> searchPanes;

/**
*
* @return a {@link Map} of {@link Column} indexed by name
Expand Down Expand Up @@ -121,4 +131,21 @@ public void addOrder(String columnName, boolean ascending) {
}
}

public void parseSearchPanesFromQueryParams(Map<String, String> queryParams, Collection<String> attributes) {
Map<String, Set<String>> searchPanes = new HashMap<>();
attributes.forEach(attribute -> searchPanes.put(attribute, new HashSet<>()));

queryParams.forEach((key, value) -> {
Matcher matcher = SEARCH_PANES_REGEX.matcher(key);
if (matcher.matches()) {
String attribute = matcher.group(1);
if (attributes.contains(attribute)) {
searchPanes.get(attribute).add(value);
}
}
});

this.searchPanes = searchPanes;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ public class DataTablesOutput<T> {
@JsonView(View.class)
private List<T> data = Collections.emptyList();

/**
* Output for the <a href="https://datatables.net/extensions/searchpanes/">SearchPanes extension</a>
*/
@JsonView(View.class)
private SearchPanes searchPanes;

/**
* Optional: If an error occurs during the running of the server-side processing script, you can
* inform the user of this error by passing back the error message to be displayed using this
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.springframework.data.jpa.datatables.mapping;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.List;
import java.util.Map;

@Data
@AllArgsConstructor
public class SearchPanes {
private Map<String, List<Item>> options;

@Data
@AllArgsConstructor
public static class Item {
private String label;
private String value;
private long total;
private long count;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,29 @@

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Ops;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.PathBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.datatables.PredicateBuilder;
import org.springframework.data.jpa.datatables.mapping.DataTablesInput;
import org.springframework.data.jpa.datatables.mapping.DataTablesOutput;
import org.springframework.data.jpa.datatables.mapping.SearchPanes;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.QuerydslJpaRepository;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.SimpleEntityPathResolver;

import javax.persistence.EntityManager;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import static com.querydsl.core.types.dsl.Expressions.stringOperation;

public class QDataTablesRepositoryImpl<T, ID extends Serializable>
extends QuerydslJpaRepository<T, ID> implements QDataTablesRepository<T, ID> {

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

if (input.getSearchPanes() != null) {
output.setSearchPanes(computeSearchPanes(input, predicate));
}
} catch (Exception e) {
output.setError(e.toString());
}

return output;
}

private SearchPanes computeSearchPanes(DataTablesInput input, Predicate predicate) {
Map<String, List<SearchPanes.Item>> options = new HashMap<>();

input.getSearchPanes().forEach((attribute, values) -> {
List<SearchPanes.Item> items = new ArrayList<>();
PathBuilder<Object> path = this.builder.get(attribute);

this.createQuery()
.select(stringOperation(Ops.STRING_CAST, path), path.count())
.where(predicate)
.groupBy(path)
.fetchResults()
.getResults()
.forEach(tuple -> {
String value = tuple.get(0, String.class);
long count = tuple.get(1, Long.class);
items.add(new SearchPanes.Item(value, value, count, count));
});

options.put(attribute, items);
});

return new SearchPanes(options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,31 @@
import org.springframework.data.jpa.datatables.SpecificationBuilder;
import org.springframework.data.jpa.datatables.mapping.DataTablesInput;
import org.springframework.data.jpa.datatables.mapping.DataTablesOutput;
import org.springframework.data.jpa.datatables.mapping.SearchPanes;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public class DataTablesRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
implements DataTablesRepository<T, ID> {
private final EntityManager entityManager;

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

super(entityInformation, entityManager);
this.entityManager = entityManager;
}

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

SpecificationBuilder<T> specificationBuilder = new SpecificationBuilder<>(input);
Page<T> data = findAll(
Specification.where(specificationBuilder.build())
.and(additionalSpecification)
.and(preFilteringSpecification),
specificationBuilder.createPageable());
Specification<T> specification = Specification.where(specificationBuilder.build())
.and(additionalSpecification)
.and(preFilteringSpecification);
Page<T> data = findAll(specification, specificationBuilder.createPageable());

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

if (input.getSearchPanes() != null) {
output.setSearchPanes(computeSearchPanes(input, specification));
}
} catch (Exception e) {
output.setError(e.toString());
}

return output;
}

private SearchPanes computeSearchPanes(DataTablesInput input, Specification<T> specification) {
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
Map<String, List<SearchPanes.Item>> options = new HashMap<>();

input.getSearchPanes().forEach((attribute, values) -> {
CriteriaQuery<Object[]> query = criteriaBuilder.createQuery(Object[].class);
Root<T> root = query.from(getDomainClass());
query.multiselect(root.get(attribute), criteriaBuilder.count(root));
query.groupBy(root.get(attribute));
query.where(specification.toPredicate(root, query, criteriaBuilder));
root.getFetches().clear();

List<SearchPanes.Item> items = new ArrayList<>();

this.entityManager.createQuery(query).getResultList().forEach(objects -> {
String value = String.valueOf(objects[0]);
long count = (long) objects[1];
items.add(new SearchPanes.Item(value, value, count, count));
});

options.put(attribute, items);
});

return new SearchPanes(options);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.springframework.data.jpa.datatables.mapping;

import org.junit.Test;

import java.util.*;

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;

public class DataTablesInputTest {

@Test
public void testParseSearchPanes() {
DataTablesInput input = new DataTablesInput();
HashMap<String, String> queryParams = new HashMap<>();
queryParams.put("searchPanes.attr1.0", "1");
queryParams.put("searchPanes.attr1.1", "2");
queryParams.put("searchPanes.attr2.0", "3");
queryParams.put("searchPanes.attr3.test", "4");
queryParams.put("searchPanes.attr4.0", "5");
queryParams.put("ignored", "6");

input.parseSearchPanesFromQueryParams(queryParams, asList("attr1", "attr2"));

assertThat(input.getSearchPanes()).containsOnly(
entry("attr1", new HashSet<>(asList("1", "2"))),
entry("attr2", new HashSet<>(asList("3")))
);
}
}
Loading

0 comments on commit 16803f9

Please sign in to comment.