lista de usuarios terminada

This commit is contained in:
2025-09-26 17:57:06 +02:00
parent 01a1ac4b71
commit 88b43847f0
29 changed files with 27681 additions and 138 deletions

View File

@ -12,68 +12,117 @@ import java.util.function.Function;
public class DataTable<T> {
public interface FilterHook<T> extends BiConsumer<SpecBuilder<T>, DataTablesRequest> {}
public interface SpecBuilder<T> { void add(Specification<T> extra); }
public interface FilterHook<T> extends BiConsumer<SpecBuilder<T>, DataTablesRequest> {
}
public interface SpecBuilder<T> {
void add(Specification<T> extra);
}
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<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 Specification<T> baseSpec = (root, q, cb) -> cb.conjunction();
private final ObjectMapper om = new ObjectMapper();
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;
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;
}
public static <T> DataTable<T> of(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt, List<String> searchable) {
public static <T> DataTable<T> of(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
List<String> searchable) {
return new DataTable<>(repo, entityClass, dt, searchable);
}
/** Equivalente a tu $q->where(...): establece condición base */
public DataTable<T> where(Specification<T> spec) { this.baseSpec = this.baseSpec.and(spec); return this; }
public DataTable<T> where(Specification<T> spec) {
this.baseSpec = this.baseSpec.and(spec);
return this;
}
/** add("campo", fn(entity)->valor|Map) */
public DataTable<T> add(String field, Function<T, Object> fn) {
adders.add(entity -> {
Map<String,Object> m = new HashMap<>();
Map<String, Object> m = new HashMap<>();
m.put(field, fn.apply(entity));
return m;
});
return this;
}
/** add(fn(entity)->Map<String,Object>) para devolver objetos anidados como tu "logo" */
public DataTable<T> add(Function<T, Map<String,Object>> fn) { adders.add(fn); return this; }
/**
* add(fn(entity)->Map<String,Object>) para devolver objetos anidados como tu
* "logo"
*/
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 */
/**
* edit("campo", fn(entity)->valor) sobreescribe un campo existente o lo crea si
* no existe
*/
public DataTable<T> edit(String field, Function<T, Object> fn) {
editors.add(row -> { row.put(field, fn.apply((T)row.get("__entity"))); return row; });
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<T> filter(FilterHook<T> hook) { filters.add(hook); return this; }
public DataTable<T> filter(FilterHook<T> hook) {
filters.add(hook);
return this;
}
public DataTablesResponse<Map<String,Object>> toJson(long totalCount) {
public DataTablesResponse<Map<String, Object>> toJson(long totalCount) {
// Construye spec con búsqueda global + base + filtros custom
Specification<T> spec = baseSpec.and(DataTablesSpecification.build(dt, searchable));
final Specification<T>[] holder = new Specification[]{ spec };
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
Sort sort = Sort.unsorted();
if (!dt.order.isEmpty() && !dt.columns.isEmpty()) {
List<Sort.Order> orders = new ArrayList<>();
for (var o : dt.order) {
String field = dt.columns.get(o.column).name;
orders.add(new Sort.Order("desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC, field));
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;
}
}
}
sort = Sort.by(orders);
}
// Page
@ -84,12 +133,14 @@ public class DataTable<T> {
long filtered = p.getTotalElements();
// Mapear entidad -> Map base (via Jackson) + add/edit
List<Map<String,Object>> data = new ArrayList<>();
List<Map<String, Object>> data = new ArrayList<>();
for (T e : p.getContent()) {
Map<String,Object> row = om.convertValue(e, Map.class);
Map<String, Object> 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);
for (var ad : adders)
row.putAll(ad.apply(e));
for (var ed : editors)
ed.apply(row);
row.remove("__entity");
data.add(row);
}

View File

@ -12,10 +12,9 @@ public class DataTablesService {
public static <T> DataTablesResponse<T> handle(
DataTablesRequest dt,
JpaSpecificationExecutor<T> repo,
long totalCount, // count sin filtros (cacheable)
long totalCount, // count sin filtros (cacheable)
List<String> searchableFields,
Class<T> entityClass
) {
Class<T> entityClass) {
// Spec (filtros)
Specification<T> spec = DataTablesSpecification.build(dt, searchableFields);
@ -23,11 +22,37 @@ public class DataTablesService {
Sort sort = Sort.unsorted();
if (!dt.order.isEmpty() && !dt.columns.isEmpty()) {
List<Sort.Order> orders = new ArrayList<>();
for (DataTablesRequest.Order o : dt.order) {
String field = dt.columns.get(o.column).name;
orders.add(new Sort.Order("desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC, field));
for (var o : dt.order) {
var col = dt.columns.get(o.column);
String field = col != null ? col.name : null;
// Acepta solo columnas válidas: no vacías, marcadas como orderable y en la
// whitelist "searchable"
if (field == null || field.isBlank())
continue;
if (!col.orderable)
continue;
if (!searchableFields.contains(field))
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 {
// Fallback: primera columna de dt.columns que sea orderable y esté en la
// whitelist
for (var c : dt.columns) {
if (c != null && c.orderable && c.name != null && !c.name.isBlank()
&& searchableFields.contains(c.name)) {
sort = Sort.by(c.name);
break;
}
}
// Si no hay ninguna válida, sort se queda UNSORTED
}
sort = Sort.by(orders);
}
// Page
@ -42,7 +67,6 @@ public class DataTablesService {
dt.draw,
totalCount,
filtered,
result.getContent()
);
result.getContent());
}
}

View File

@ -8,30 +8,42 @@ import java.util.List;
public class DataTablesSpecification {
/**
* Crea una Specification con búsqueda global y por columna (LIKE case-insensitive)
* @param dt request de datatables
* Crea una Specification con búsqueda global y por columna (LIKE
* case-insensitive)
*
* @param dt request de datatables
* @param searchableFields campos del entity para el buscador global
*/
public static <T> Specification<T> build(DataTablesRequest dt, List<String> searchableFields) {
return (root, query, cb) -> {
List<Predicate> ands = new ArrayList<>();
// Filtro por columna (si quieres soportarlo)
// Filtro por columna (si lo usas en el cliente)
for (int i = 0; i < dt.columns.size(); i++) {
DataTablesRequest.Column col = dt.columns.get(i);
if (col.searchable && col.search != null && col.search.value != null && !col.search.value.isEmpty()) {
ands.add(like(cb, root.get(col.name), col.search.value));
try {
ands.add(like(cb, root.get(col.name), col.search.value));
} catch (IllegalArgumentException ex) {
// columna no mapeada o relación: la ignoramos
}
}
}
// Búsqueda global
if (dt.search != null && dt.search.value != null && !dt.search.value.isEmpty() && !searchableFields.isEmpty()) {
if (dt.search != null && dt.search.value != null && !dt.search.value.isEmpty()
&& !searchableFields.isEmpty()) {
String term = "%" + dt.search.value.trim().toLowerCase() + "%";
List<Predicate> ors = new ArrayList<>();
for (String f : searchableFields) {
ors.add(cb.like(cb.lower(root.get(f).as(String.class)), term));
try {
ors.add(cb.like(cb.lower(root.get(f).as(String.class)), term));
} catch (IllegalArgumentException ex) {
// campo no simple: lo saltamos
}
}
ands.add(cb.or(ors.toArray(new Predicate[0])));
if (!ors.isEmpty())
ands.add(cb.or(ors.toArray(new Predicate[0])));
}
return ands.isEmpty() ? cb.conjunction() : cb.and(ands.toArray(new Predicate[0]));