Skip to content

Commit ef76554

Browse files
Add dynamic field filtering to RestNavigationController for enhanced query capabilities
1 parent e78dfe7 commit ef76554

File tree

1 file changed

+115
-0
lines changed

1 file changed

+115
-0
lines changed

platform/core/web/src/main/java/tools/dynamia/web/navigation/RestNavigationController.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import tools.dynamia.commons.logger.LoggingService;
3434
import tools.dynamia.crud.CrudPage;
3535
import tools.dynamia.domain.query.DataPaginator;
36+
import tools.dynamia.domain.query.QueryConditions;
3637
import tools.dynamia.domain.query.QueryParameters;
3738
import tools.dynamia.domain.services.CrudService;
3839
import tools.dynamia.domain.util.QueryBuilder;
@@ -50,6 +51,7 @@
5051

5152
import java.util.List;
5253
import java.util.Map;
54+
import java.util.Set;
5355

5456
/**
5557
* REST controller that handles CRUD operations on entities exposed through the application's
@@ -202,6 +204,7 @@ private ResponseEntity<String> readAll(String path, HttpServletRequest request)
202204
query.where(pageParams);
203205
}
204206
parseConditions(query, readDescriptor);
207+
applyRequestFilters(request, query, readDescriptor);
205208

206209
int pageSize = getParameterNumber(request, "size");
207210
int currentPage = getParameterNumber(request, "page");
@@ -473,6 +476,118 @@ public static void parseConditions(QueryBuilder query, ViewDescriptor descriptor
473476
}
474477
}
475478

479+
/**
480+
* Applies dynamic field filters derived from HTTP query parameters to the given {@link QueryBuilder}.
481+
*
482+
* <p>Any request parameter whose name does not start with {@code _} and is not a reserved
483+
* pagination keyword ({@code page}, {@code size}) is treated as a potential field filter.
484+
* The filter value is matched against the entity field registered in the {@link ViewDescriptor}
485+
* and the appropriate {@link QueryConditions} condition is selected based on the field's Java type:</p>
486+
*
487+
* <ul>
488+
* <li>{@link String} — {@code LIKE} with auto-searchable wildcard wrapping</li>
489+
* <li>{@link Number} / numeric primitives — exact equality ({@code =})</li>
490+
* <li>{@link Boolean} / {@code boolean} — exact equality ({@code =})</li>
491+
* <li>{@link Enum} subtypes — exact equality ({@code =}) using {@link Enum#valueOf}</li>
492+
* <li>Any other type — skipped; not safe to cast without type information</li>
493+
* </ul>
494+
*
495+
* <p>Parameters for fields not present in the descriptor are silently ignored.</p>
496+
*
497+
* <p><b>Usage example:</b></p>
498+
* <pre>{@code GET /api/users?name=john&status=ACTIVE&age=30 }</pre>
499+
*
500+
* @param request the current HTTP request carrying the filter parameters
501+
* @param query the query builder to augment with filter conditions
502+
* @param descriptor the view descriptor used to resolve field metadata; if {@code null} this
503+
* method does nothing
504+
*/
505+
public static void applyRequestFilters(HttpServletRequest request, QueryBuilder query, ViewDescriptor descriptor) {
506+
if (descriptor == null) {
507+
return;
508+
}
509+
510+
// Parameter names that are reserved for pagination / internal use and must not be treated as filters.
511+
Set<String> RESERVED_PARAMS = Set.of("page", "size");
512+
513+
request.getParameterMap().forEach((paramName, values) -> {
514+
// Skip internal (_) params and pagination keywords
515+
if (paramName.startsWith("_") || RESERVED_PARAMS.contains(paramName) || values.length == 0) {
516+
return;
517+
}
518+
519+
Field field = descriptor.getField(paramName);
520+
if (field == null) {
521+
return; // Unknown field — ignore silently
522+
}
523+
524+
Class<?> fieldType = field.getFieldClass();
525+
if (fieldType == null && field.getPropertyInfo() != null) {
526+
fieldType = field.getPropertyInfo().getType();
527+
}
528+
if (fieldType == null) {
529+
return; // Cannot determine type — skip
530+
}
531+
532+
String rawValue = values[0];
533+
if (rawValue == null || rawValue.isBlank()) {
534+
return;
535+
}
536+
537+
try {
538+
if (fieldType == String.class) {
539+
// LIKE with auto-searchable wildcard (e.g., "john" → "%john%")
540+
query.and(paramName, QueryConditions.like(rawValue));
541+
542+
} else if (Number.class.isAssignableFrom(fieldType) || fieldType.isPrimitive() && fieldType != boolean.class) {
543+
// Numeric equality — parse to the right numeric type
544+
Object numericValue = parseNumber(rawValue, fieldType);
545+
if (numericValue != null) {
546+
query.and(paramName, QueryConditions.eq(numericValue));
547+
}
548+
549+
} else if (fieldType == Boolean.class || fieldType == boolean.class) {
550+
boolean boolValue = "true".equalsIgnoreCase(rawValue) || "1".equals(rawValue);
551+
query.and(paramName, QueryConditions.eq(boolValue));
552+
553+
} else if (fieldType.isEnum()) {
554+
@SuppressWarnings({"unchecked", "rawtypes"})
555+
Object enumValue = Enum.valueOf((Class<Enum>) fieldType, rawValue.toUpperCase());
556+
query.and(paramName, QueryConditions.eq(enumValue));
557+
558+
}
559+
// Date, entity references, collections, etc. are intentionally skipped:
560+
// they require more complex handling and are out of scope for simple URL filtering.
561+
} catch (Exception e) {
562+
LoggingService.get(RestNavigationController.class)
563+
.warn("Ignoring filter param '" + paramName + "=" + rawValue + "': " + e.getMessage());
564+
}
565+
});
566+
}
567+
568+
/**
569+
* Parses a raw string value into the target numeric type.
570+
*
571+
* <p>Supports {@link Integer}, {@code int}, {@link Long}, {@code long},
572+
* {@link Double}, {@code double}, {@link Float}, {@code float},
573+
* {@link Short}, {@code short}, {@link Byte}, {@code byte},
574+
* and {@link java.math.BigDecimal}.</p>
575+
*
576+
* @param raw the raw string to parse
577+
* @param targetType the target numeric {@link Class}
578+
* @return the parsed number, or {@code null} if the type is not supported
579+
*/
580+
private static Object parseNumber(String raw, Class<?> targetType) {
581+
if (targetType == Integer.class || targetType == int.class) return Integer.parseInt(raw);
582+
if (targetType == Long.class || targetType == long.class) return Long.parseLong(raw);
583+
if (targetType == Double.class || targetType == double.class) return Double.parseDouble(raw);
584+
if (targetType == Float.class || targetType == float.class) return Float.parseFloat(raw);
585+
if (targetType == Short.class || targetType == short.class) return Short.parseShort(raw);
586+
if (targetType == Byte.class || targetType == byte.class) return Byte.parseByte(raw);
587+
if (targetType == java.math.BigDecimal.class) return new java.math.BigDecimal(raw);
588+
return null;
589+
}
590+
476591
/**
477592
* Reads an integer request parameter by name. Returns {@code 0} if the parameter
478593
* is absent or cannot be parsed as an integer.

0 commit comments

Comments
 (0)