package com.imprimelibros.erp.datatables; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; 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 { /* ===== Tipos funcionales ===== */ public interface FilterHook extends BiConsumer, DataTablesRequest> { } public interface SpecBuilder { void add(Specification extra); } /** * Filtro custom por campo virtual: te doy (root, query, cb, value) y me * devuelves un Predicate */ @FunctionalInterface public interface FieldFilter { Predicate apply(Root 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 { Expression apply(Root root, CriteriaQuery query, CriteriaBuilder cb); } /* ===== Estado ===== */ private final JpaSpecificationExecutor repo; private final Class entityClass; private final DataTablesRequest dt; private final List searchable; private final List>> adders = new ArrayList<>(); private final List, Map>> editors = new ArrayList<>(); private final List> filters = new ArrayList<>(); private Specification baseSpec = (root, q, cb) -> cb.conjunction(); private final ObjectMapper om = new ObjectMapper() .registerModule(new JavaTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); /** whitelist de campos ordenables “simples” (por nombre) */ private List orderable = null; /** mapas de comportamiento custom por campo */ private final Map> orderCustom = new HashMap<>(); private final Map> filterCustom = new HashMap<>(); private boolean onlyAdded = false; /* ===== Ctor / factory ===== */ private DataTable(JpaSpecificationExecutor repo, Class entityClass, DataTablesRequest dt, List searchable) { this.repo = repo; this.entityClass = entityClass; this.dt = dt; this.searchable = searchable != null ? searchable : List.of(); } public static DataTable of(JpaSpecificationExecutor repo, Class entityClass, DataTablesRequest dt, List searchable) { return new DataTable<>(repo, entityClass, dt, searchable); } /* ===== Fluent API ===== */ public DataTable onlyAddedColumns() { this.onlyAdded = true; return this; } /** WHERE base reusable */ public DataTable where(Specification spec) { this.baseSpec = this.baseSpec.and(spec); return this; } /** Campos renderizados */ public DataTable add(String field, Function fn) { adders.add(entity -> { Map m = new HashMap<>(); m.put(field, fn.apply(entity)); return m; }); return this; } public DataTable addIf(boolean condition, String field, Function fn) { if (condition) return add(field, fn); return this; } public DataTable add(Function> fn) { adders.add(fn); return this; } /** Edita/inyecta valor usando la entidad original (guardada como __entity) */ @SuppressWarnings("unchecked") public DataTable edit(String field, Function fn) { editors.add(row -> { row.put(field, fn.apply((T) row.get("__entity"))); return row; }); return this; } /** Whitelist de campos simples ordenables (por nombre) */ public DataTable orderable(List fields) { this.orderable = fields; return this; } /** Orden custom por campo virtual (expresiones) */ public DataTable orderable(String field, FieldOrder orderFn) { this.orderCustom.put(field, orderFn); return this; } /** Filtro custom por campo virtual (LIKE, rangos, etc.) */ public DataTable filter(String field, FieldFilter filterFn) { this.filterCustom.put(field, filterFn); return this; } /** Hook para añadir Specifications extra programáticamente */ public DataTable filter(FilterHook hook) { filters.add(hook); return this; } /* ===== Helpers ===== */ private List getOrderable() { return (orderable == null || orderable.isEmpty()) ? this.searchable : this.orderable; } /* ===== Core ===== */ public DataTablesResponse> toJson(long totalCount) { // 1) Spec base + búsqueda (global/columnas) + hooks programáticos Specification spec = baseSpec.and(DataTablesSpecification.build(dt, searchable)); final Specification[] holder = new Specification[] { spec }; // 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 simpleOrders = new ArrayList<>(); boolean customApplied = false; if (!dt.order.isEmpty() && !dt.columns.isEmpty()) { for (var o : dt.order) { var col = dt.columns.get(o.column); if (col == null) continue; String field = col.name; if (field == null || field.isBlank()) continue; if (!col.orderable) continue; if (!getOrderable().contains(field)) continue; if (orderCustom.containsKey(field)) { final boolean asc = !"desc".equalsIgnoreCase(o.dir); final FieldOrder 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)); } } } 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(); // 5) Mapeo a Map + add/edit List> data = new ArrayList<>(); for (T e : p.getContent()) { Map 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) ed.apply(row); 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(); } }