AnalyticsConverterHelper.java
package com.smartsupplypro.inventory.service.impl.analytics;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import com.smartsupplypro.inventory.exception.InvalidRequestException;
/**
* Type conversion utilities for analytics database projections.
*
* <p>Handles type coercion from native SQL queries (Object[] projections) to Java types,
* accounting for vendor differences between H2 (test) and Oracle (prod).
*
* <p><strong>Supported Conversions</strong>:
* <ul>
* <li>DATE/TIMESTAMP → LocalDate/LocalDateTime</li>
* <li>Numeric projections → Number (handles null as zero)</li>
* <li>String normalization (blank → null)</li>
* <li>Date window defaults (30-day lookback)</li>
* </ul>
*
* @author Smart Supply Pro Development Team
* @version 1.0.0
* @since 2.0.0
*/
final class AnalyticsConverterHelper {
private AnalyticsConverterHelper() {
// Utility class - prevent instantiation
}
// ==================================================================================
// Date/Time Conversions
// ==================================================================================
/**
* Converts a date-like value to {@link LocalDate}.
*
* <p>Accepts:
* <ul>
* <li>{@link LocalDate}</li>
* <li>{@link java.sql.Date} (converted via {@code toLocalDate()})</li>
* <li>{@link java.sql.Timestamp} (converted via {@code toLocalDateTime().toLocalDate()})</li>
* <li>{@link CharSequence} in formats starting with {@code yyyy-MM-dd}</li>
* </ul>
*
* @param o raw value from native projections (DATE/TIMESTAMP/STRING)
* @return the corresponding {@link LocalDate}
* @throws IllegalStateException if the value cannot be interpreted as a date
*/
static LocalDate asLocalDate(Object o) {
if (o instanceof LocalDate ld) return ld;
if (o instanceof java.sql.Date d) return d.toLocalDate();
if (o instanceof java.sql.Timestamp ts) return ts.toLocalDateTime().toLocalDate();
if (o instanceof CharSequence cs) {
String s = cs.toString();
if (s.length() >= 10) {
// e.g. "2025-03-15 00:00:00.0" → "2025-03-15"
return LocalDate.parse(s.substring(0, 10));
}
}
// Last resort: try toString().substring(0,10) if it looks like a timestamp literal
String s = String.valueOf(o);
if (s != null && s.length() >= 10 && s.charAt(4) == '-' && s.charAt(7) == '-') {
return LocalDate.parse(s.substring(0, 10));
}
throw new IllegalStateException("Expected LocalDate/Date/Timestamp/String but got: " +
(o == null ? "null" : o.getClass().getName() + " -> " + o));
}
/**
* Converts a timestamp-like object to {@link LocalDateTime}.
*
* @param o timestamp value (LocalDateTime or java.sql.Timestamp)
* @return the corresponding {@link LocalDateTime}
* @throws IllegalStateException if the object type is unsupported
*/
static LocalDateTime asLocalDateTime(Object o) {
if (o instanceof LocalDateTime ldt) return ldt;
if (o instanceof Timestamp ts) return ts.toLocalDateTime();
throw new IllegalStateException("Expected LocalDateTime or java.sql.Timestamp but got: " + o);
}
/**
* Safely unboxes any numeric projection value via {@link Number}.
*
* @param o numeric value (null, Number, or BigDecimal)
* @return the corresponding {@link Number} (null treated as zero)
*/
static Number asNumber(Object o) {
if (o == null) return java.math.BigDecimal.ZERO;
if (o instanceof Number n) return n;
if (o instanceof java.math.BigDecimal bd) return bd;
throw new IllegalStateException("Expected numeric type but got: " + o);
}
// ==================================================================================
// Date Window Utilities
// ==================================================================================
/**
* Applies defaults for a date window (last 30 days ending today) and validates {@code start <= end}.
*
* @param start nullable inclusive start date
* @param end nullable inclusive end date
* @return a 2-element array containing the effective start and end
* @throws InvalidRequestException if the effective start is after the effective end
*/
static LocalDate[] defaultAndValidateDateWindow(LocalDate start, LocalDate end) {
LocalDate s = (start == null) ? LocalDate.now().minusDays(30) : start;
LocalDate e = (end == null) ? LocalDate.now() : end;
if (s.isAfter(e)) {
throw new InvalidRequestException("start must be on or before end");
}
return new LocalDate[]{s, e};
}
/**
* Converts date to start of day (00:00:00.000000000).
*
* @param d the date
* @return LocalDateTime at start of day
*/
static LocalDateTime startOfDay(LocalDate d) {
return LocalDateTime.of(d, LocalTime.MIN);
}
/**
* Converts date to end of day (23:59:59.999999999).
*
* @param d the date
* @return LocalDateTime at end of day
*/
static LocalDateTime endOfDay(LocalDate d) {
return LocalDateTime.of(d, LocalTime.MAX);
}
// ==================================================================================
// String Utilities
// ==================================================================================
/**
* Normalizes a String to {@code null} if blank; otherwise returns a trimmed value.
*
* @param s the string to normalize
* @return null if blank, trimmed value otherwise
*/
static String blankToNull(String s) {
return (s == null || s.trim().isEmpty()) ? null : s.trim();
}
/**
* Ensures a String is non-blank; returns trimmed value or throws.
*
* @param v the value to check
* @param name the parameter name for error message
* @return trimmed non-blank value
* @throws InvalidRequestException if value is blank
*/
static String requireNonBlank(String v, String name) {
if (v == null || v.trim().isEmpty()) {
throw new InvalidRequestException(name + " must not be blank");
}
return v.trim();
}
/**
* Ensures a reference is non-null; returns it or throws.
*
* @param v the value to check
* @param name the parameter name for error message
* @return the non-null value
* @throws InvalidRequestException if value is null
*/
static <T> T requireNonNull(T v, String name) {
if (v == null) {
throw new InvalidRequestException(name + " must not be null");
}
return v;
}
}