mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 08:58:48 +00:00
falta presupuesto marcapaginas y maquetacion y revision general en admin. revisar user
This commit is contained in:
@ -7,12 +7,14 @@ import org.springframework.data.domain.*;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import jakarta.persistence.criteria.*;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class DataTable<T> {
|
||||
|
||||
/* ===== Tipos funcionales ===== */
|
||||
public interface FilterHook<T> extends BiConsumer<SpecBuilder<T>, DataTablesRequest> {
|
||||
}
|
||||
|
||||
@ -20,31 +22,55 @@ public class DataTable<T> {
|
||||
void add(Specification<T> extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtro custom por campo virtual: te doy (root, query, cb, value) y me
|
||||
* devuelves un Predicate
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FieldFilter<T> {
|
||||
Predicate apply(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb, String value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Orden custom por campo virtual: te doy (root, query, cb) y me devuelves la
|
||||
* Expression<?> para orderBy
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FieldOrder<T> {
|
||||
Expression<?> apply(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
|
||||
}
|
||||
|
||||
/* ===== Estado ===== */
|
||||
private final JpaSpecificationExecutor<T> repo;
|
||||
private final Class<T> entityClass;
|
||||
private final DataTablesRequest dt;
|
||||
private final List<String> searchable;
|
||||
|
||||
private final List<Function<T, Map<String, Object>>> adders = new ArrayList<>();
|
||||
private final List<Function<Map<String, Object>, Map<String, Object>>> editors = new ArrayList<>();
|
||||
private final List<FilterHook<T>> filters = new ArrayList<>();
|
||||
private Specification<T> baseSpec = (root, q, cb) -> cb.conjunction();
|
||||
|
||||
private final ObjectMapper om = new ObjectMapper()
|
||||
.registerModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
.registerModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
/** whitelist de campos ordenables “simples” (por nombre) */
|
||||
private List<String> orderable = null;
|
||||
|
||||
private boolean onlyAdded = false;
|
||||
public DataTable<T> onlyAddedColumns(){
|
||||
this.onlyAdded = true;
|
||||
return this;
|
||||
}
|
||||
/** mapas de comportamiento custom por campo */
|
||||
private final Map<String, FieldOrder<T>> orderCustom = new HashMap<>();
|
||||
private final Map<String, FieldFilter<T>> filterCustom = new HashMap<>();
|
||||
|
||||
private boolean onlyAdded = false;
|
||||
|
||||
/* ===== Ctor / factory ===== */
|
||||
private DataTable(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||
List<String> searchable) {
|
||||
this.repo = repo;
|
||||
this.entityClass = entityClass;
|
||||
this.dt = dt;
|
||||
this.searchable = searchable;
|
||||
this.searchable = searchable != null ? searchable : List.of();
|
||||
}
|
||||
|
||||
public static <T> DataTable<T> of(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||
@ -52,13 +78,19 @@ public class DataTable<T> {
|
||||
return new DataTable<>(repo, entityClass, dt, searchable);
|
||||
}
|
||||
|
||||
/** Equivalente a tu $q->where(...): establece condición base */
|
||||
/* ===== Fluent API ===== */
|
||||
public DataTable<T> onlyAddedColumns() {
|
||||
this.onlyAdded = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** WHERE base reusable */
|
||||
public DataTable<T> where(Specification<T> spec) {
|
||||
this.baseSpec = this.baseSpec.and(spec);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** add("campo", fn(entity)->valor|Map) */
|
||||
/** Campos renderizados */
|
||||
public DataTable<T> add(String field, Function<T, Object> fn) {
|
||||
adders.add(entity -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
@ -68,19 +100,19 @@ public class DataTable<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* add(fn(entity)->Map<String,Object>) para devolver objetos anidados como tu
|
||||
* "logo"
|
||||
*/
|
||||
public DataTable<T> addIf(boolean condition, String field, Function<T, Object> fn) {
|
||||
if (condition)
|
||||
return add(field, fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DataTable<T> add(Function<T, Map<String, Object>> fn) {
|
||||
adders.add(fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* edit("campo", fn(entity)->valor) sobreescribe un campo existente o lo crea si
|
||||
* no existe
|
||||
*/
|
||||
/** Edita/inyecta valor usando la entidad original (guardada como __entity) */
|
||||
@SuppressWarnings("unchecked")
|
||||
public DataTable<T> edit(String field, Function<T, Object> fn) {
|
||||
editors.add(row -> {
|
||||
row.put(field, fn.apply((T) row.get("__entity")));
|
||||
@ -89,73 +121,132 @@ public class DataTable<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Whitelist de campos simples ordenables (por nombre) */
|
||||
public DataTable<T> orderable(List<String> fields) {
|
||||
this.orderable = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<String> getOrderable() {
|
||||
return (orderable == null || orderable.isEmpty()) ? this.searchable : this.orderable;
|
||||
/** Orden custom por campo virtual (expresiones) */
|
||||
public DataTable<T> orderable(String field, FieldOrder<T> orderFn) {
|
||||
this.orderCustom.put(field, orderFn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** filter((builder, req) -> builder.add(miExtraSpec(req))) */
|
||||
/** Filtro custom por campo virtual (LIKE, rangos, etc.) */
|
||||
public DataTable<T> filter(String field, FieldFilter<T> filterFn) {
|
||||
this.filterCustom.put(field, filterFn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Hook para añadir Specifications extra programáticamente */
|
||||
public DataTable<T> filter(FilterHook<T> hook) {
|
||||
filters.add(hook);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* ===== Helpers ===== */
|
||||
private List<String> getOrderable() {
|
||||
return (orderable == null || orderable.isEmpty()) ? this.searchable : this.orderable;
|
||||
}
|
||||
|
||||
/* ===== Core ===== */
|
||||
public DataTablesResponse<Map<String, Object>> toJson(long totalCount) {
|
||||
// Construye spec con búsqueda global + base + filtros custom
|
||||
// 1) Spec base + búsqueda (global/columnas) + hooks programáticos
|
||||
Specification<T> spec = baseSpec.and(DataTablesSpecification.build(dt, searchable));
|
||||
final Specification<T>[] holder = new Specification[] { spec };
|
||||
filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt));
|
||||
spec = holder[0];
|
||||
|
||||
// Sort
|
||||
// Sort
|
||||
// Hooks externos
|
||||
filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt));
|
||||
|
||||
// 2) Filtros por columna “custom” (virtuales)
|
||||
for (var col : dt.columns) {
|
||||
if (col == null || !col.searchable)
|
||||
continue;
|
||||
if (col.name == null || col.name.isBlank())
|
||||
continue;
|
||||
if (!filterCustom.containsKey(col.name))
|
||||
continue;
|
||||
if (col.search == null || col.search.value == null || col.search.value.isBlank())
|
||||
continue;
|
||||
|
||||
var value = col.search.value;
|
||||
var filterFn = filterCustom.get(col.name);
|
||||
holder[0] = holder[0].and((root, query, cb) -> {
|
||||
Predicate p = filterFn.apply(root, query, cb, value);
|
||||
return p != null ? p : cb.conjunction();
|
||||
});
|
||||
}
|
||||
|
||||
// 3) Orden:
|
||||
// - Para campos “simples” (no custom): con Sort (Spring)
|
||||
// - Para campos “custom” (virtuales/expresiones): query.orderBy(...) dentro de
|
||||
// una spec
|
||||
Sort sort = Sort.unsorted();
|
||||
List<Sort.Order> simpleOrders = new ArrayList<>();
|
||||
boolean customApplied = false;
|
||||
|
||||
if (!dt.order.isEmpty() && !dt.columns.isEmpty()) {
|
||||
List<Sort.Order> orders = new ArrayList<>();
|
||||
for (var o : dt.order) {
|
||||
var col = dt.columns.get(o.column);
|
||||
String field = col != null ? col.name : null;
|
||||
if (col == null)
|
||||
continue;
|
||||
|
||||
String field = col.name;
|
||||
if (field == null || field.isBlank())
|
||||
continue;
|
||||
if (!col.orderable)
|
||||
continue;
|
||||
if (!getOrderable().contains(field))
|
||||
continue; // << usa tu whitelist
|
||||
continue;
|
||||
|
||||
orders.add(new Sort.Order(
|
||||
"desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
|
||||
field));
|
||||
}
|
||||
if (!orders.isEmpty()) {
|
||||
sort = Sort.by(orders);
|
||||
} else {
|
||||
for (var c : dt.columns) {
|
||||
if (c != null && c.orderable && c.name != null && !c.name.isBlank()
|
||||
&& getOrderable().contains(c.name)) {
|
||||
sort = Sort.by(c.name);
|
||||
break;
|
||||
}
|
||||
if (orderCustom.containsKey(field)) {
|
||||
final boolean asc = !"desc".equalsIgnoreCase(o.dir);
|
||||
final FieldOrder<T> orderFn = orderCustom.get(field);
|
||||
|
||||
// aplica el ORDER BY custom dentro de la Specification (con Criteria)
|
||||
holder[0] = holder[0].and((root, query, cb) -> {
|
||||
Expression<?> expr = orderFn.apply(root, query, cb);
|
||||
if (expr != null) {
|
||||
query.orderBy(asc ? cb.asc(expr) : cb.desc(expr));
|
||||
}
|
||||
return cb.conjunction();
|
||||
});
|
||||
customApplied = true;
|
||||
} else {
|
||||
// orden simple por nombre de propiedad real
|
||||
simpleOrders.add(new Sort.Order(
|
||||
"desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
|
||||
field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page
|
||||
if (!simpleOrders.isEmpty()) {
|
||||
sort = Sort.by(simpleOrders);
|
||||
}
|
||||
|
||||
// 4) Paginación (Sort para simples; custom order ya va dentro de la spec)
|
||||
int page = dt.length > 0 ? dt.start / dt.length : 0;
|
||||
Pageable pageable = dt.length > 0 ? PageRequest.of(page, dt.length, sort) : Pageable.unpaged();
|
||||
|
||||
var p = repo.findAll(holder[0], pageable);
|
||||
long filtered = p.getTotalElements();
|
||||
|
||||
// Mapear entidad -> Map base (via Jackson) + add/edit
|
||||
// 5) Mapeo a Map + add/edit
|
||||
List<Map<String, Object>> data = new ArrayList<>();
|
||||
for (T e : p.getContent()) {
|
||||
Map<String, Object> row = onlyAdded ? new HashMap<>() : om.convertValue(e, Map.class);
|
||||
row.put("__entity", e); // para editores que necesiten la entidad
|
||||
Map<String, Object> row;
|
||||
if (onlyAdded) {
|
||||
row = new HashMap<>();
|
||||
} else {
|
||||
try {
|
||||
row = om.convertValue(e, Map.class);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
row = new HashMap<>();
|
||||
}
|
||||
}
|
||||
row.put("__entity", e);
|
||||
for (var ad : adders)
|
||||
row.putAll(ad.apply(e));
|
||||
for (var ed : editors)
|
||||
@ -163,6 +254,12 @@ public class DataTable<T> {
|
||||
row.remove("__entity");
|
||||
data.add(row);
|
||||
}
|
||||
|
||||
return new DataTablesResponse<>(dt.draw, totalCount, filtered, data);
|
||||
}
|
||||
}
|
||||
|
||||
private Predicate nullSafePredicate(CriteriaBuilder cb) {
|
||||
// Devuelve conjunción para no interferir con los demás predicados
|
||||
return cb.conjunction();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user