| /*
|
| * 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
|
| * <p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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> T nonull(Object value, T defaultValue) {
|
| return value == null ? defaultValue : (T) value;
|
| }
|
|
|
| /**
|
| * nonull<p>
|
| * 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<p>
|
| * Unchecked cast helper (avoid using unless necessary).
|
| *
|
| * @param value object
|
| * @param type target type
|
| * @return casted value
|
| */
|
| @SuppressWarnings("unchecked")
|
| public static <T> T value(Object value, Class<T> type) {
|
| return (T) value;
|
| }
|
|
|
| /**
|
| * createMap<p>
|
| * Convenience to create a Map from varargs key/value pairs.
|
| *
|
| * @param keyValue alternating key, value
|
| * @return map with pairs
|
| */
|
| @SuppressWarnings("unchecked")
|
| public static <K extends Object, V extends Object> Map<K, V> createMap(Object... keyValue) {
|
| Map<K, V> map = (Map<K, V>) new HashMap<Object, Object>();
|
| for (int i = 0; i < keyValue.length; i += 2) {
|
| ((Map<Object, Object>) map).put(keyValue[i], keyValue[i + 1]);
|
| }
|
| return map;
|
| }
|
|
|
| /* --------------------- Date arithmetic --------------------- */
|
|
|
| /**
|
| * addDays<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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<p>
|
| * Iterable wrapper to iterate a List in reverse order without copying.
|
| *
|
| * @param <T> element type
|
| */
|
| public static class Reversed<T> implements Iterable<T> {
|
| private final List<T> original;
|
|
|
| public Reversed(List<T> original) {
|
| this.original = original;
|
| }
|
|
|
| public Iterator<T> iterator() {
|
| return new Iterator<T>() {
|
| 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<p>
|
| * Returns a reversed-iteration view of a list.
|
| *
|
| * @param original list to view
|
| * @param <T> element type
|
| * @return iterable reverse view
|
| */
|
| public static <T> Reversed<T> reversedList(List<T> original) {
|
| return new Reversed<T>(original);
|
| }
|
|
|
| /**
|
| * isSorted<p>
|
| * Checks if a list of Doubles is non-decreasing.
|
| *
|
| * @param original list
|
| * @return true if sorted ascending
|
| */
|
| public static boolean isSorted(List<Double> 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<p>
|
| * 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<List<String>> readFileLines(File file, final String separator) throws IOException {
|
| List<String> lines = Files.readAllLines(Paths.get(file.toURI()), Charset.forName("utf8"));
|
| return lines.stream().map(line -> Arrays.asList(line.split(separator))).collect(Collectors.toList());
|
| }
|
|
|
| /**
|
| * readStreamLines<p>
|
| * 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<List<String>> 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<List<String>> lines = new ArrayList<>();
|
| while ((line = br.readLine()) != null) {
|
| lines.add(Arrays.asList(line.split(separator)));
|
| }
|
| return lines;
|
| }
|
| }
|
|
|
| /**
|
| * deleteDirectoryTree<p>
|
| * 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<Path>() {
|
| @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<p>
|
| * 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<Path>() {
|
| @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<p>
|
| * 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<Boolean> waitFor(Supplier<Boolean> condition, long timeout) throws TimeoutException {
|
| return CompletableFuture.<Boolean>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<p>
|
| * 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<String> getLastLines(File file, int numLines) throws IOException {
|
| LinkedList<String> 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<p>
|
| * Returns a sorted list without duplicates using a set roundtrip.
|
| *
|
| * @param list input list
|
| * @param <T> type must be Comparable
|
| * @return sorted distinct list
|
| */
|
| public static <T> List<T> removeDuplicates(List<T> list) {
|
| return new HashSet<>(list).stream().sorted().collect(Collectors.toList());
|
| }
|
|
|
| /* --------------------- Reflection helpers --------------------- */
|
|
|
| /**
|
| * setProperty<p>
|
| * 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<p>
|
| * 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<p>
|
| * 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);
|
| }
|
| }
|
| }
|