/* * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. */ package net.curisit.securis.utils; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeoutException; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.beanutils.PropertyUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import net.curisit.integrity.exception.CurisRuntimeException; /** * Utils *

* General-purpose static utilities: hashing, dates/times, collections, I/O helpers, * simple email validation, reflection helpers, file tailing, etc. * * Author: JRA * Last reviewed by JRA on Oct 6, 2025. */ public class Utils { private static final Logger LOG = LogManager.getLogger(Utils.class); /* --------------------- Hash functions --------------------- */ /** * md5

* Returns MD5 digest (hex, 32 chars) for the input text. * * @param str source text * @return lowercase hex MD5 or null on error */ public static String md5(String str) { try { MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(str.getBytes(), 0, str.length()); BigInteger i = new BigInteger(1, mDigest.digest()); return String.format("%1$032x", i); } catch (NoSuchAlgorithmException e) { LOG.error("Error generating MD5 for string: " + str, e); } return null; } /** * sha1

* Returns SHA-1 digest (hex, 40 chars) for the input text. * * @param str source text * @return lowercase hex SHA-1 or null on error */ public static String sha1(String str) { try { MessageDigest mDigest = MessageDigest.getInstance("SHA1"); mDigest.update(str.getBytes(), 0, str.length()); BigInteger i = new BigInteger(1, mDigest.digest()); return String.format("%1$040x", i); } catch (NoSuchAlgorithmException e) { LOG.error("Error generating SHA1 for string: " + str, e); } return null; } /** * sha256

* Returns SHA-256 digest (hex, 64 chars) for the input text. * * @param str source text * @return lowercase hex SHA-256 or null on error */ public static String sha256(String str) { return sha256(str.getBytes()); } /** * sha256

* Returns SHA-256 digest (hex) for a byte array. * * @param bytes data * @return hex SHA-256 or null on error */ public static String sha256(byte[] bytes) { try { MessageDigest mDigest = MessageDigest.getInstance("SHA-256"); mDigest.update(bytes, 0, bytes.length); BigInteger i = new BigInteger(1, mDigest.digest()); return String.format("%1$064x", i); } catch (NoSuchAlgorithmException e) { LOG.error("Error generating SHA-256 for bytes: " + bytes, e); } return null; } /** * sha256

* Incrementally updates SHA-256 with multiple byte arrays and returns hex digest. * * @param bytes multiple byte chunks * @return hex SHA-256 or null on error */ public static String sha256(byte[]... bytes) { try { MessageDigest mDigest = MessageDigest.getInstance("SHA-256"); for (byte[] bs : bytes) { mDigest.update(bs, 0, bs.length); } BigInteger i = new BigInteger(1, mDigest.digest()); return String.format("%1$064x", i); } catch (NoSuchAlgorithmException e) { LOG.error("Error generating SHA-256 for bytes: " + bytes, e); } return null; } /* --------------------- ISO date helpers --------------------- */ private static final String ISO_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; private static final SimpleDateFormat sdf = new SimpleDateFormat(ISO_PATTERN); static { sdf.setTimeZone(TimeZone.getTimeZone("GMT")); } /** * toIsoFormat

* Formats a Date to ISO-8601-like string with milliseconds in GMT. * * @param date input date * @return formatted string */ public static synchronized String toIsoFormat(Date date) { return sdf.format(date); } private static final String ISO_PATTERN_REDUCED_PRECISION = "yyyy-MM-dd'T'HH:mm:ssZ"; private static final SimpleDateFormat sdfReduced = new SimpleDateFormat(ISO_PATTERN_REDUCED_PRECISION); static { sdfReduced.setTimeZone(TimeZone.getTimeZone("GMT")); } /** * toIsoFormatReduced

* Formats a Date to ISO string without milliseconds in GMT. * * @param date input date * @return formatted string */ public static synchronized String toIsoFormatReduced(Date date) { return sdfReduced.format(date); } /** * toDateFromIso

* Parses a string in {@link #ISO_PATTERN} into a Date (GMT). * * @param dateStr string to parse * @return Date or null if parsing fails */ public static synchronized Date toDateFromIso(String dateStr) { try { return sdf.parse(dateStr); } catch (ParseException e) { LOG.error("Error parsing string '{}' to Date object with pattern: {}", dateStr, ISO_PATTERN); return null; } } /* --------------------- Null-safe helpers --------------------- */ /** * nonull

* Returns {@code value} cast to T, or {@code defaultValue} if null. * * @param value input * @param defaultValue fallback * @return value or default */ @SuppressWarnings("unchecked") public static T nonull(Object value, T defaultValue) { return value == null ? defaultValue : (T) value; } /** * nonull

* Returns String value or empty string if null. * * @param value input * @return non-null string */ public static String nonull(Object value) { return nonull(value, ""); } /** * value

* Unchecked cast helper (avoid using unless necessary). * * @param value object * @param type target type * @return casted value */ @SuppressWarnings("unchecked") public static T value(Object value, Class type) { return (T) value; } /** * createMap

* Convenience to create a Map from varargs key/value pairs. * * @param keyValue alternating key, value * @return map with pairs */ @SuppressWarnings("unchecked") public static Map createMap(Object... keyValue) { Map map = (Map) new HashMap(); for (int i = 0; i < keyValue.length; i += 2) { ((Map) map).put(keyValue[i], keyValue[i + 1]); } return map; } /* --------------------- Date arithmetic --------------------- */ /** * addDays

* Adds a number of days to a Date. * * @param date base date * @param days positive/negative days * @return new Date */ public static Date addDays(Date date, int days) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.add(Calendar.DATE, days); return cal.getTime(); } /** * addHours

* Adds hours to a Date. * * @param date base date * @param hours number of hours * @return new Date */ public static Date addHours(Date date, int hours) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.add(Calendar.HOUR, hours); return cal.getTime(); } /** * addMinutes

* Adds minutes to a Date. * * @param date base date * @param minutes number of minutes * @return new Date */ public static Date addMinutes(Date date, int minutes) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.add(Calendar.MINUTE, minutes); return cal.getTime(); } /** * getStrTime

* Formats seconds as H:MM:SS (H omitted if 0). Negative yields "??". * * @param seconds total seconds * @return formatted time */ public static String getStrTime(int seconds) { if (seconds < 0) return "??"; String secs = String.format("%02d", seconds % 60); String hours = ""; String mins = "0:"; if (seconds > 3600) { hours = String.format("%d:", seconds / 3600); } if (seconds > 60) { mins = String.format("%02d:", (seconds / 60) % 60); } return hours + mins + secs; } /* --------------------- Collections & conversions --------------------- */ /** * areAllEmpty

* Checks if all provided collections are empty. * * @param lists vararg of collections * @return true if all empty */ public static boolean areAllEmpty(Collection... lists) { for (Collection list : lists) { if (!list.isEmpty()) return false; } return true; } /** * toDateFromLocalDate

* Converts a {@link LocalDate} to {@link Date} at system default zone start of day. * * @param ld local date * @return Date or null */ public static Date toDateFromLocalDate(LocalDate ld) { if (ld == null) return null; Instant instant = ld.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(); return Date.from(instant); } /** * toLocalDateFromDate

* Converts a {@link Date} to {@link LocalDate} at system default zone. * * @param date Date * @return LocalDate or null */ public static LocalDate toLocalDateFromDate(Date date) { if (date == null) return null; Instant instant = Instant.ofEpochMilli(date.getTime()); return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).toLocalDate(); } /** * compare

* Null-safe equals: both null → true; otherwise {@code equals()}. * * @param obj1 first object * @param obj2 second object * @return equality result */ public static boolean compare(Object obj1, Object obj2) { if (obj1 == null) { return obj2 == null; } else { return obj1.equals(obj2); } } /* --------------------- Email validation --------------------- */ private static final String EMAIL_PATTERN_STR = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_PATTERN_STR); /** * isValidEmail

* Validates an email address format with a basic regex. * * @param email candidate value * @return valid or not */ public static boolean isValidEmail(final String email) { if (email == null) { return false; } return EMAIL_PATTERN.matcher(email).matches(); } /* --------------------- Iterable helpers --------------------- */ /** * Reversed

* Iterable wrapper to iterate a List in reverse order without copying. * * @param element type */ public static class Reversed implements Iterable { private final List original; public Reversed(List original) { this.original = original; } public Iterator iterator() { return new Iterator() { int cursor = original.size(); public boolean hasNext() { return cursor > 0; } public T next() { return original.get(--cursor); } public void remove() { throw new CurisRuntimeException("ERROR removing item in read-only iterator"); } }; } } /** * reversedList

* Returns a reversed-iteration view of a list. * * @param original list to view * @param element type * @return iterable reverse view */ public static Reversed reversedList(List original) { return new Reversed(original); } /** * isSorted

* Checks if a list of Doubles is non-decreasing. * * @param original list * @return true if sorted ascending */ public static boolean isSorted(List original) { for (int i = 0; i < (original.size() - 1); i++) { Double v0 = original.get(i); Double v1 = original.get(i + 1); if (v0.doubleValue() > v1.doubleValue()) { return false; } } return true; } /* --------------------- Simple file reading helpers --------------------- */ /** * readFileLines

* Reads a file as UTF-8 and splits each line by a separator. * * @param file source file * @param separator regex separator * @return list of line fields * @throws IOException on I/O errors */ public static List> readFileLines(File file, final String separator) throws IOException { List lines = Files.readAllLines(Paths.get(file.toURI()), Charset.forName("utf8")); return lines.stream().map(line -> Arrays.asList(line.split(separator))).collect(Collectors.toList()); } /** * readStreamLines

* Reads an InputStream as UTF-8 and splits each line by a separator. * * @param stream input stream (caller closes it) * @param separator regex separator * @return list of line fields * @throws IOException on I/O errors */ public static List> readStreamLines(InputStream stream, final String separator) throws IOException { try ( InputStreamReader isr = new InputStreamReader(stream, Charset.forName("UTF-8")); BufferedReader br = new BufferedReader(isr); ) { String line; List> lines = new ArrayList<>(); while ((line = br.readLine()) != null) { lines.add(Arrays.asList(line.split(separator))); } return lines; } } /** * deleteDirectoryTree

* Recursively deletes a directory and its contents. * * @param baseDirectory directory path * @throws IOException on failure */ public static void deleteDirectoryTree(Path baseDirectory) throws IOException { Files.walkFileTree(baseDirectory, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.deleteIfExists(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.deleteIfExists(dir); return FileVisitResult.CONTINUE; } }); } /* --------------------- Directory size & tail --------------------- */ private static class LongWrapper { long num = 0L; } /** * getDirectoryContentSize

* Computes total size (bytes) of files within a directory tree. * * @param baseDirectory root path * @return total bytes * @throws IOException on traversal errors */ public static long getDirectoryContentSize(Path baseDirectory) throws IOException { final LongWrapper size = new LongWrapper(); Files.walkFileTree(baseDirectory, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { size.num += file.toFile().length(); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { if (!dir.equals(baseDirectory)) { size.num += dir.toFile().length(); } return FileVisitResult.CONTINUE; } }); return size.num; } /** * waitFor

* Asynchronously waits until a condition is true or a timeout is reached. * * @param condition supplier that should eventually return true * @param timeout max wait (ms) * @return future completing with true if condition met * @throws TimeoutException surfaced through CompletionException if timed out */ public static CompletableFuture waitFor(Supplier condition, long timeout) throws TimeoutException { return CompletableFuture.supplyAsync(() -> { long t0 = System.currentTimeMillis(); while(!condition.get()) { long t1 = System.currentTimeMillis(); if ((t1 - t0) > timeout) { throw new CompletionException(new TimeoutException()); } try { Thread.sleep(100); } catch (InterruptedException e) { throw new CompletionException(e); } } return true; }); } private static final int BLOCK_SIZE = 4096; // 4KB /** * getLastLines

* Efficiently reads the last N lines of a text file by scanning from the end in blocks. * * @param file file to read * @param numLines number of lines from the end * @return list of lines in natural order (oldest→newest among the last N) * @throws IOException on I/O errors */ public static List getLastLines(File file, int numLines) throws IOException { LinkedList lines = new LinkedList<>(); long fileSize = file.length(); int chunk = (fileSize < BLOCK_SIZE) ? (int)fileSize : BLOCK_SIZE; long seekPos = fileSize - chunk; FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.READ); final byte newLine = (byte)'\n'; String partialLine = null; while (lines.size() < numLines) { ByteBuffer dst = ByteBuffer.allocate(chunk); int bytesRead = fc.read(dst, seekPos); byte[] content = dst.array(); int lastLineIdx = bytesRead; for (int i = bytesRead - 1; i > 0 ; i--) { byte b = content[i]; if (b == newLine) { if (i < (bytesRead - 1) || partialLine != null) { String line = new String(Arrays.copyOfRange(content, i, lastLineIdx)).trim(); if (partialLine != null) { line += partialLine; partialLine = null; } lines.addFirst(line); if (lines.size() == numLines) { return lines; } } lastLineIdx = i; } } if (lastLineIdx > 0) { if (partialLine == null) { partialLine = new String(Arrays.copyOfRange(content, 0, lastLineIdx)).trim(); } else { partialLine = new String(Arrays.copyOfRange(content, 0, lastLineIdx)) + partialLine; } } if (seekPos == 0) { lines.addFirst(partialLine); break; } if (seekPos < BLOCK_SIZE) { chunk = (int)seekPos; seekPos = 0; } else { seekPos -= BLOCK_SIZE; } } return lines; } /** * removeDuplicates

* Returns a sorted list without duplicates using a set roundtrip. * * @param list input list * @param type must be Comparable * @return sorted distinct list */ public static List removeDuplicates(List list) { return new HashSet<>(list).stream().sorted().collect(Collectors.toList()); } /* --------------------- Reflection helpers --------------------- */ /** * setProperty

* Sets a bean property by name using Apache BeanUtils PropertyUtils. * * @param bean target bean * @param fieldName property name (nested supported) * @param value value to assign * @throws RuntimeException wrapping reflection errors */ public static void setProperty(Object bean, String fieldName, Object value) { try { PropertyUtils.setProperty(bean, fieldName, value); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { LOG.error("Missing field:" + fieldName); throw new RuntimeException(e); } } /** * getProperty

* Reads a bean property by name using Apache PropertyUtils. * * @param bean target bean * @param fieldName property name * @return value * @throws RuntimeException wrapping reflection errors */ public static Object getProperty(Object bean, String fieldName) { try { return PropertyUtils.getProperty(bean, fieldName); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { LOG.error("Missing field:" + fieldName); throw new RuntimeException(e); } } /** * cloneBean

* Clones a bean using Apache BeanUtils' copy mechanisms (shallow copy). * * @param bean source bean * @return new cloned bean * @throws CurisRuntimeException on cloning errors */ public static Object cloneBean(Object bean) { try { return BeanUtils.cloneBean(bean); } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { throw new CurisRuntimeException("Error cloning bean", e); } } }