This commit is contained in:
Francesco 2023-09-19 12:55:41 +02:00
parent a91cddc7a2
commit 2db84d9996
8 changed files with 192 additions and 65 deletions

View File

@ -22,11 +22,13 @@ import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.persistence.criteria.Predicate;
import tech.ailef.dbadmin.DbAdmin; import tech.ailef.dbadmin.DbAdmin;
import tech.ailef.dbadmin.dbmapping.DbAdminRepository; import tech.ailef.dbadmin.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.dbmapping.DbObject; import tech.ailef.dbadmin.dbmapping.DbObject;
import tech.ailef.dbadmin.dbmapping.DbObjectSchema; import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.dto.PaginatedResult; import tech.ailef.dbadmin.dto.PaginatedResult;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.exceptions.InvalidPageException; import tech.ailef.dbadmin.exceptions.InvalidPageException;
@Controller @Controller
@ -90,7 +92,9 @@ public class DefaultDbAdminController {
public String list(Model model, @PathVariable String className, public String list(Model model, @PathVariable String className,
@RequestParam(required=false) Integer page, @RequestParam(required=false) String query, @RequestParam(required=false) Integer page, @RequestParam(required=false) String query,
@RequestParam(required=false) Integer pageSize, @RequestParam(required=false) String sortKey, @RequestParam(required=false) Integer pageSize, @RequestParam(required=false) String sortKey,
@RequestParam(required=false) String sortOrder) { @RequestParam(required=false) String sortOrder, @RequestParam MultiValueMap<String, String> otherParams) {
System.out.println(otherParams);
if (page == null) page = 1; if (page == null) page = 1;
if (pageSize == null) pageSize = 50; if (pageSize == null) pageSize = 50;
@ -98,8 +102,8 @@ public class DefaultDbAdminController {
try { try {
PaginatedResult result = null; PaginatedResult result = null;
if (query != null) { if (query != null || !otherParams.isEmpty()) {
result = repository.search(schema, query, page, pageSize, sortKey, sortOrder); result = repository.search(schema, query, page, pageSize, sortKey, sortOrder, otherParams);
} else { } else {
result = repository.findAll(schema, page, pageSize, sortKey, sortOrder); result = repository.findAll(schema, page, pageSize, sortKey, sortOrder);
} }
@ -337,9 +341,12 @@ public class DefaultDbAdminController {
} }
} }
@GetMapping("/settings") @GetMapping("/settings")
public String settings(Model model) { public String settings(Model model) {
model.addAttribute("activePage", "settings"); model.addAttribute("activePage", "settings");
return "settings"; return "settings";
} }
} }

View File

@ -0,0 +1,27 @@
package tech.ailef.dbadmin.controller;
import java.util.Map;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import jakarta.servlet.http.HttpServletRequest;
/**
* This class registers some ModelAttribute objects that are
* used in all templates.
*/
@ControllerAdvice
public class GlobalController {
/**
* A multi valued map containing the query parameters. It is used primarily
* in building complex URL when performing faceted search with multiple filters.
* @param request the incoming request
* @return multi valued map of request parameters
*/
@ModelAttribute("queryParams")
public Map<String, String[]> getQueryParams(HttpServletRequest request) {
return request.getParameterMap();
}
}

View File

@ -5,6 +5,7 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.util.MultiValueMap;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaBuilder;
@ -12,6 +13,7 @@ import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Root;
import tech.ailef.dbadmin.exceptions.DbAdminException;
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public class AdvancedJpaRepository extends SimpleJpaRepository { public class AdvancedJpaRepository extends SimpleJpaRepository {
@ -27,47 +29,40 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
this.schema = schema; this.schema = schema;
} }
public long count(String q) { @SuppressWarnings("unchecked")
public long count(String q, MultiValueMap<String, String> filteringParams) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(Long.class); CriteriaQuery query = cb.createQuery(Long.class);
Root root = query.from(schema.getJavaClass()); Root root = query.from(schema.getJavaClass());
List<DbField> stringFields =
schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING)
.collect(Collectors.toList());
System.out.println("STRING F = " + stringFields);
List<Predicate> predicates = new ArrayList<>();
for (DbField f : stringFields) {
Path path = root.get(f.getJavaName());
predicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%"));
}
List<Predicate> finalPredicates = buildPredicates(q, filteringParams, cb, root);
query.select(cb.count(root.get(schema.getPrimaryKey().getName()))) query.select(cb.count(root.get(schema.getPrimaryKey().getName())))
.where(cb.or(predicates.toArray(new Predicate[predicates.size()]))); .where(
cb.and(
finalPredicates.toArray(new Predicate[finalPredicates.size()])
)
);
Object o = entityManager.createQuery(query).getSingleResult(); Object o = entityManager.createQuery(query).getSingleResult();
return (Long)o; return (Long)o;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List<Object> search(String q, int page, int pageSize, String sortKey, String sortOrder) { public List<Object> search(String q, int page, int pageSize, String sortKey, String sortOrder, MultiValueMap<String, String> filteringParams) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(schema.getJavaClass()); CriteriaQuery query = cb.createQuery(schema.getJavaClass());
Root root = query.from(schema.getJavaClass()); Root root = query.from(schema.getJavaClass());
List<DbField> stringFields = List<Predicate> finalPredicates = buildPredicates(q, filteringParams, cb, root);
schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING)
.collect(Collectors.toList());
List<Predicate> predicates = new ArrayList<>();
for (DbField f : stringFields) {
Path path = root.get(f.getJavaName());
predicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%"));
}
query.select(root) query.select(root)
.where(cb.or(predicates.toArray(new Predicate[predicates.size()]))); .where(
cb.and(
finalPredicates.toArray(new Predicate[finalPredicates.size()]) // query search on String fields
)
);
if (sortKey != null) if (sortKey != null)
query.orderBy(sortOrder.equals("DESC") ? cb.desc(root.get(sortKey)) : cb.asc(root.get(sortKey))); query.orderBy(sortOrder.equals("DESC") ? cb.desc(root.get(sortKey)) : cb.asc(root.get(sortKey)));
@ -75,19 +70,72 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
.setFirstResult((page - 1) * pageSize).getResultList(); .setFirstResult((page - 1) * pageSize).getResultList();
} }
public List<Object> distinctFieldValues(DbField field) { private List<Predicate> buildPredicates(String q, MultiValueMap<String, String> filteringParams,
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaBuilder cb, Path root) {
List<Predicate> finalPredicates = new ArrayList<>();
Class<?> outputType = field.getType().getJavaClass();
if (field.getConnectedType() != null) {
outputType = field.getConnectedSchema().getPrimaryKey().getType().getJavaClass();
}
CriteriaQuery query = cb.createQuery(outputType);
Root root = query.from(schema.getJavaClass());
query.select(root.get(field.getJavaName()).as(outputType)).distinct(true); List<DbField> stringFields =
schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING)
.collect(Collectors.toList());
return entityManager.createQuery(query).getResultList(); List<Predicate> queryPredicates = new ArrayList<>();
if (q != null) {
for (DbField f : stringFields) {
Path path = root.get(f.getJavaName());
queryPredicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%"));
}
Predicate queryPredicate = cb.or(queryPredicates.toArray(new Predicate[queryPredicates.size()]));
finalPredicates.add(queryPredicate);
}
/*
* Compute filtering predicates
*/
if (filteringParams != null) {
List<String> ops = filteringParams.get("filter_op[]");
List<String> fields = filteringParams.get("filter_field[]");
List<String> values = filteringParams.get("filter_value[]");
if (ops != null && fields != null && values != null) {
if (ops.size() != fields.size() || fields.size() != values.size()
|| ops.size() != values.size()) {
throw new DbAdminException("Filtering parameters must have the same size");
}
for (int i = 0; i < ops.size(); i++) {
String op = ops.get(i);
String field = fields.get(i);
String value = values.get(i);
if (op.equalsIgnoreCase("equals")) {
finalPredicates.add(cb.equal(cb.toString(root.get(field)), value));
} else if (op.equalsIgnoreCase("contains")) {
System.out.println("CONTAINS");
finalPredicates.add(cb.like(cb.toString(root.get(field)), "%" + value + "%"));
}
}
}
}
return finalPredicates;
} }
// @SuppressWarnings("unchecked")
// public List<Object> distinctFieldValues(DbField field) {
// CriteriaBuilder cb = entityManager.getCriteriaBuilder();
//
// Class<?> outputType = field.getType().getJavaClass();
// if (field.getConnectedType() != null) {
// outputType = field.getConnectedSchema().getPrimaryKey().getType().getJavaClass();
// }
//
// CriteriaQuery query = cb.createQuery(outputType);
// Root root = query.from(schema.getJavaClass());
//
// query.select(root.get(field.getJavaName()).as(outputType)).distinct(true);
//
// return entityManager.createQuery(query).getResultList();
// }
} }

View File

@ -16,6 +16,7 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@ -63,8 +64,8 @@ public class DbAdminRepository {
* @param query * @param query
* @return * @return
*/ */
public long count(DbObjectSchema schema, String query) { public long count(DbObjectSchema schema, String query, MultiValueMap<String, String> filteringParams) {
return schema.getJpaRepository().count(query); return schema.getJpaRepository().count(query, filteringParams);
} }
@ -223,10 +224,11 @@ public class DbAdminRepository {
* @param query * @param query
* @return * @return
*/ */
public PaginatedResult search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey, String sortOrder) { public PaginatedResult search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey,
String sortOrder, MultiValueMap<String, String> filteringParams) {
AdvancedJpaRepository jpaRepository = schema.getJpaRepository(); AdvancedJpaRepository jpaRepository = schema.getJpaRepository();
long maxElement = count(schema, query); long maxElement = count(schema, query, filteringParams);
int maxPage = (int)(Math.ceil ((double)maxElement / pageSize)); int maxPage = (int)(Math.ceil ((double)maxElement / pageSize));
if (page <= 0) page = 1; if (page <= 0) page = 1;
@ -236,7 +238,7 @@ public class DbAdminRepository {
return new PaginatedResult( return new PaginatedResult(
new PaginationInfo(page, maxPage, pageSize, maxElement), new PaginationInfo(page, maxPage, pageSize, maxElement),
jpaRepository.search(query, page, pageSize, sortKey, sortOrder).stream() jpaRepository.search(query, page, pageSize, sortKey, sortOrder, filteringParams).stream()
.map(o -> new DbObject(o, schema)) .map(o -> new DbObject(o, schema))
.toList() .toList()
); );
@ -251,7 +253,7 @@ public class DbAdminRepository {
public List<DbObject> search(DbObjectSchema schema, String query) { public List<DbObject> search(DbObjectSchema schema, String query) {
AdvancedJpaRepository jpaRepository = schema.getJpaRepository(); AdvancedJpaRepository jpaRepository = schema.getJpaRepository();
return jpaRepository.search(query, 1, 50, null, null).stream() return jpaRepository.search(query, 1, 50, null, null, null).stream()
.map(o -> new DbObject(o, schema)) .map(o -> new DbObject(o, schema))
.toList(); .toList();
} }

View File

@ -183,9 +183,9 @@ public class DbObjectSchema {
}).toList(); }).toList();
} }
public List<Object> getFieldValues(DbField field) { // public List<Object> getFieldValues(DbField field) {
return jpaRepository.distinctFieldValues(field); // return jpaRepository.distinctFieldValues(field);
} // }
public Object[] getInsertArray(Map<String, String> params, Map<String, MultipartFile> files) { public Object[] getInsertArray(Map<String, String> params, Map<String, MultipartFile> files) {
int currentIndex = 0; int currentIndex = 0;

View File

@ -4,4 +4,5 @@ spring.datasource.username=sa
spring.datasource.password=password spring.datasource.password=password
#spring.h2.console.enabled=true #spring.h2.console.enabled=true
spring.jpa.show-sql=true

View File

@ -38,5 +38,55 @@
</div> </div>
</div> </div>
<div class="card mb-3 filterable-field" th:fragment="filter_field(field)">
<div class="card-header noselect cursor-pointer">
<i class="bi bi-caret-right filter-icon align-middle"></i>
<span class="fw-bold align-middle" th:text="${field.getName()}"></span>
</div>
<div class="card-body d-none">
<form action="" method="GET">
<th:block th:each="p : ${queryParams.keySet()}">
<input th:each="v : ${queryParams.get(p)}" th:name="${p}" th:value="${v}" type="hidden">
</th:block>
<div class="input-group pe-2">
<th:block th:if="${field.isForeignKey()}">
<div th:replace="~{fragments/forms :: input_autocomplete(field=${field}, value='')}">
</div>
<input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|">
</th:block>
<th:block th:unless="${field.isForeignKey()}">
<div class="container w-25">
<select class="form-select w-auto" name="filter_op[]">
<option value="equals">Equals</option>
<option value="contains">Contains</option>
</select>
</div>
<input type="hidden" name="filter_field[]" th:value="${field.getName()}">
<input placeholder="NULL" th:type="${field.getType().getHTMLName()}"
name="filter_value[]"
class="form-control" th:id="|__id_${field.getName()}|"
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
th:required="${!field.isNullable() && !field.isPrimaryKey()}"
step="any"
oninvalid="this.setCustomValidity('This field is not nullable.')"
oninput="this.setCustomValidity('')">
<input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|">
</th:block>
<button class="ui-btn btn btn-primary">Filter</button>
</div>
</form>
<!--
<th:block th:if="${field.getConnectedType() != null}">
<div th:each="val : ${schema.getFieldValues(field)}">
<span th:text="${val}"></span>
</div>
</th:block>
-->
</div>
</div>
</body> </body>
</html> </html>

View File

@ -45,7 +45,7 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h3 class="fw-bold mb-4 align-baseline w-100"> <h3 class="fw-bold mb-4 align-baseline flex-grow-1">
<span title="Java class name"> [[ ${schema.getJavaClass().getSimpleName()} ]] </span> <span title="Java class name"> [[ ${schema.getJavaClass().getSimpleName()} ]] </span>
<span title="Database table name" class="ms-3 label label-primary label-gray font-monospace"> <span title="Database table name" class="ms-3 label label-primary label-gray font-monospace">
[[ ${schema.getTableName()} ]] [[ ${schema.getTableName()} ]]
@ -55,7 +55,8 @@
</h3> </h3>
<h3><a title="Create new item" <h3><a title="Create new item"
th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a></h3> th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a>
</h3>
</div> </div>
<div th:replace="~{fragments/table_selectable :: table(results=${page.getResults()}, schema=${schema})}"> <div th:replace="~{fragments/table_selectable :: table(results=${page.getResults()}, schema=${schema})}">
@ -66,20 +67,11 @@
<div class="box"> <div class="box">
<h3 class="fw-bold mb-3"><i class="bi bi-funnel"></i> Filters</h3> <h3 class="fw-bold mb-3"><i class="bi bi-funnel"></i> Filters</h3>
<div class="card mb-3 filterable-field" th:each="field : ${schema.getFilterableFields()}">
<div class="card-header noselect cursor-pointer">
<i class="bi bi-caret-right filter-icon align-middle"></i> <th:block th:each="field : ${schema.getFilterableFields()}">
<span class="fw-bold align-middle" th:text="${field.getName()}"></span> <div th:replace="~{fragments/forms :: filter_field(field=${field})}"></div>
</div> </th:block>
<div class="card-body d-none">
<th:block th:if="${field.getConnectedType() != null}">
<div th:each="val : ${schema.getFieldValues(field)}">
<span th:text="${val}"></span>
</div>
</th:block>
</div>
</div>
</div> </div>
</div> </div>