package com.imprimelibros.erp.datatables; 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 java.util.*; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; public class DataTable { public interface FilterHook extends BiConsumer, DataTablesRequest> { } public interface SpecBuilder { void add(Specification extra); } 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(); private DataTable(JpaSpecificationExecutor repo, Class entityClass, DataTablesRequest dt, List searchable) { this.repo = repo; this.entityClass = entityClass; this.dt = dt; this.searchable = searchable; } public static DataTable of(JpaSpecificationExecutor repo, Class entityClass, DataTablesRequest dt, List searchable) { return new DataTable<>(repo, entityClass, dt, searchable); } /** Equivalente a tu $q->where(...): establece condición base */ public DataTable where(Specification spec) { this.baseSpec = this.baseSpec.and(spec); return this; } /** add("campo", fn(entity)->valor|Map) */ public DataTable add(String field, Function fn) { adders.add(entity -> { Map m = new HashMap<>(); m.put(field, fn.apply(entity)); return m; }); return this; } /** * add(fn(entity)->Map) para devolver objetos anidados como tu * "logo" */ public DataTable add(Function> fn) { adders.add(fn); return this; } /** * edit("campo", fn(entity)->valor) sobreescribe un campo existente o lo crea si * no existe */ public DataTable edit(String field, Function fn) { editors.add(row -> { row.put(field, fn.apply((T) row.get("__entity"))); return row; }); return this; } /** filter((builder, req) -> builder.add(miExtraSpec(req))) */ public DataTable filter(FilterHook hook) { filters.add(hook); return this; } public DataTablesResponse> toJson(long totalCount) { // Construye spec con búsqueda global + base + filtros custom Specification spec = baseSpec.and(DataTablesSpecification.build(dt, searchable)); final Specification[] holder = new Specification[] { spec }; filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt)); spec = holder[0]; // Sort // Sort Sort sort = Sort.unsorted(); if (!dt.order.isEmpty() && !dt.columns.isEmpty()) { List orders = new ArrayList<>(); for (var o : dt.order) { var col = dt.columns.get(o.column); String field = col != null ? col.name : null; if (field == null || field.isBlank()) continue; if (!col.orderable) continue; if (!searchable.contains(field)) continue; // << usa tu whitelist 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() && searchable.contains(c.name)) { sort = Sort.by(c.name); break; } } } } // Page 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 List> data = new ArrayList<>(); for (T e : p.getContent()) { Map row = om.convertValue(e, Map.class); row.put("__entity", e); // para editores que necesiten la entidad 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); } }