diff --git a/src/main/java/com/ffii/core/utils/DateUtils.java b/src/main/java/com/ffii/core/utils/DateUtils.java new file mode 100644 index 0000000..677a4d1 --- /dev/null +++ b/src/main/java/com/ffii/core/utils/DateUtils.java @@ -0,0 +1,1021 @@ +package com.ffii.core.utils; + +import java.sql.Timestamp; +import java.text.DecimalFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.time.FastDateFormat; + +/** + * Utility class for date parsing and formatting. + * + * @see FastDateFormat + * @see ISO 8601 + * @see RFC 822 + * + * @author Patrick + */ +public abstract class DateUtils extends org.apache.commons.lang3.time.DateUtils { + + /** Used to format double digit hours, minutes, and seconds */ + private static final DecimalFormat DF00 = new DecimalFormat("00"); + + /** RegEx pattern to test 24-hour 4-digit time format */ + private static final Pattern PATTERN_24HR_TIME_STRING = Pattern.compile("^(([0-1][0-9])|(2[0-3]))[0-5][0-9]$"); + + /** + * HK date formatter for date without time zone. The format used is dd/MM/yyyy. + */ + public static final FastDateFormat HK_DATE_FORMAT = FastDateFormat.getInstance("dd/MM/yyyy"); + + /** + * HK date formatter for datetime. The format used is dd/MM/yyyy HH:mm:ss. + */ + public static final FastDateFormat HK_DATETIME_FORMAT = FastDateFormat.getInstance("dd/MM/yyyy HH:mm:ss"); + + /** + * SQL date formatter for date without time zone. The format used is yyyy-MM-dd. + */ + public static final FastDateFormat SQL_DATE_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd"); + + /** + * Ext JS ISO8601 formatter for date-time with time zone. The format used is yyyy-MM-dd'T'HH:mm:ssZZ. + */ + public static final FastDateFormat ISO_JS_DATETIME_TIME_ZONE_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + + /** + * Default parse patterns + *
    + *
  1. dd/MM/yyyy + *
  2. yyyy-MM-dd + *
  3. yyyy-MM-dd'T'HH:mm:ss + *
  4. yyyy-MM-dd'T'HH:mm:ss.SSS'Z' + *
  5. yyyy-MM-dd'T'HH:mm:ss.SSSZ + *
+ */ + public static final String[] PARSE_PATTERNS = { "dd/MM/yyyy", "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm:ss", + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }; + + /** + * PayPal parse patterns + *
    + *
  1. HH:mm:ss MMM dd, yyyy z + *
+ */ + public static final String[] PARSE_PATTERNS_PAYPAL = { "HH:mm:ss MMM dd, yyyy z" }; + + /** + * Formats a Date as String using the default pattern + * + * @param date + * the date object, may be {@code null} + * + * @return the formatted date string, or "" if date is {@code null} + */ + public static String formatDate(Date date) { + return formatDate(date, StringUtils.EMPTY); + } + + /** + * Formats a Date as String using the default pattern, with a fallback default value if the date is null + * + * @param date + * the date object, may be {@code null} + * @param defaultValue + * the value to return if date is {@code null} + * + * @return the formatted date string, or the default value if date is {@code null} + */ + public static String formatDate(Date date, String defaultValue) { + if (date == null) return defaultValue; + return HK_DATE_FORMAT.format(date); + } + + /** + * Formats a Date as String using the provided pattern, with a fallback default value if the date is null + * + * @param date + * the date object, may be {@code null} + * @param pattern + * {@link java.text.SimpleDateFormat} compatible pattern + * @param defaultValue + * the value to return if date is {@code null} + * + * @return the formatted date string, or the default value if date is {@code null} + * + * @throws IllegalArgumentException + * if pattern is invalid + */ + public static String formatDate(Date date, String pattern, String defaultValue) { + if (date == null) return defaultValue; + if (HK_DATE_FORMAT.getPattern().equals(pattern)) { + return HK_DATE_FORMAT.format(date); + } else if (SQL_DATE_FORMAT.getPattern().equals(pattern)) { + return SQL_DATE_FORMAT.format(date); + } else { + return FastDateFormat.getInstance(pattern).format(date); + } + } + + /** + *

+ * Parses a string representing a date by trying a variety of different parsers. + *

+ *

+ * The parse will try each parse pattern in turn. A parse is only deemed successful if it parses the whole of the input string. If no parse patterns match, + * the default value is returned. + *

+ *

+ * The parser parses strictly - it does not allow for dates such as "February 942, 1996". + *

+ * + * @param str + * the date to parse, not null + * @param parsePatterns + * the date format patterns to use, see {@link SimpleDateFormat}, not null + * @param defaultValue + * the default value to use as fallback + * + * @return the parsed {@link Date} object + * + * @see DateUtils#parseDateStrictly(String, String[]) + */ + public static Date parseDateStrictly(String str, String[] parsePatterns, Date defaultValue) { + if (str == null) return defaultValue; + try { + return parseDateStrictly(str, parsePatterns); + } catch (ParseException e) { + return defaultValue; + } + } + + /** + *

+ * Parses a string representing a date by trying a variety of different parsers. + *

+ *

+ * The parse will try each parse pattern in turn. A parse is only deemed successful if it parses the whole of the input string. If no parse patterns match, + * the default value is returned. + *

+ *

+ * The parser parses strictly - it does not allow for dates such as "February 942, 1996". + *

+ * + * @param str + * the date to parse, not null + * @param defaultValue + * the default value to use as fallback + * + * @return the parsed {@link Date} object + * + * @see DateUtils#parseDateStrictly(String, String[]) + */ + public static Date parseDateStrictly(String str, Date defaultValue) { + return parseDateStrictly(str, PARSE_PATTERNS, defaultValue); + } + + /** + * Clone a date + * + * @return The cloned date, or null if input date is null + */ + @SuppressWarnings("unchecked") + public static T clone(Date date) { + return (date == null) ? null : (T) date.clone(); + } + + /** + * Check if the time is valid 24-hour time + */ + @Deprecated + public static boolean isValid24HourTime(int time) { + int hour = time / 100; + int minute = time - hour * 100; + + // invalid hour + if (hour < 0 || hour > 23) return false; + + // invalid minute + if (minute < 0 || minute > 59) return false; + + return true; + } + + /** + * Check if the time string is valid 24-hour format (4 digit without any symbols) + */ + public static final boolean isValid24HourTime(String timeString) { + if (timeString == null || timeString.length() != 4) { + // if it's null or not 4-digit + return false; + } else { + // else check against RegEx + return PATTERN_24HR_TIME_STRING.matcher(timeString).matches(); + } + } + + /** + * Converts 24-hour time string to a long value in millisecond + * + * @param str + * 24-hour time string in the format of "hh:mm" or "hhmm" + */ + public static long convertTimeStringToMillisecond(String str) { + if (StringUtils.isNotBlank(str)) { + str = StringUtils.remove(str, ':'); + int time = NumberUtils.toInt(str); + int hour = time / 100; + int minute = time - hour * 100; + return hour * 3600000 + minute * 60000; + } + return 0L; + } + + /** + * Returns a new Date object by adding the time value from a 24-hour time string + * + * @param clazz + * the Date class to return + * @param date + * the Date object that the time to add to, will not be changed + * @param str + * 24-hour time string in the format of "hh:mm" or "hhmm" + */ + @SuppressWarnings("unchecked") + public static T addTimeToDate(Class clazz, Date date, String str) { + if (date != null && StringUtils.isNotBlank(str)) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + long time = c.getTimeInMillis() + convertTimeStringToMillisecond(str); + if (clazz == Date.class) { + return (T) new Date(time); + } else if (clazz == java.sql.Date.class) { + return (T) new java.sql.Date(time); + } else if (clazz == Timestamp.class) { + return (T) new java.sql.Timestamp(time); + } else { + throw new UnsupportedOperationException(clazz.getName() + " is not supported."); + } + } + return null; + } + + /** + * Returns today with only the date component + */ + public static Date getToday() { + return truncate(new Date(), Calendar.DAY_OF_MONTH); + } + + /** + * Returns the calendar with HK Week of Year Standard + * + */ + public static final Calendar getCalendar() { + Calendar calendar = Calendar.getInstance(); + calendar.setMinimalDaysInFirstWeek(3); + return calendar; + } + + /** + * Returns the week number within the current year (e.g. if the date is within the first week of the year, it will return 1) + * + * @param date + * non-{@code null} {@code date} object + */ + public static final int getWeekOfYear(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + return calendar.get(Calendar.WEEK_OF_YEAR); + } + + /** + * Returns the week number within the current year (e.g. if the date is within the first week of the year, it will return 1), where you can specify the + * first day of the week + * + * @param date + * non-{@code null} {@code date} object + * @param firstDayOfWeek + * the first day of the week (e.g. Calendar.SUNDAY, Calendar.MONDAY, etc) + */ + public static final int getWeekOfYear(Date date, int firstDayOfWeek) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.setFirstDayOfWeek(firstDayOfWeek); + return calendar.get(Calendar.WEEK_OF_YEAR); + } + + /** + * Returns {@code true} if the {@code date} is earlier than today (ignoring the time component), returns {@code false} if later than or same as today + */ + public static boolean isEarlierThanToday(Date date) { + return truncate(date, Calendar.DAY_OF_MONTH).getTime() < getToday().getTime(); + } + + /** + * Returns {@code true} if the {@code date} is later than today (ignoring the time component), returns {@code false} if earlier than or same as today + */ + public static boolean isLaterThanToday(Date date) { + return truncate(date, Calendar.DAY_OF_MONTH).getTime() > getToday().getTime(); + } + + /** + * Returns {@code true} if date {@code a} is earlier than date {@code b} (ignoring the time component), returns {@code false} if later than or same day + */ + public static boolean isEarlierThan(Date a, Date b) { + return truncate(a, Calendar.DAY_OF_MONTH).getTime() < truncate(b, Calendar.DAY_OF_MONTH).getTime(); + } + + /** + * Returns {@code true} if date {@code a} is later than date {@code b} (ignoring the time component), returns {@code false} if earlier than or same day + */ + public static boolean isLaterThan(Date a, Date b) { + return truncate(a, Calendar.DAY_OF_MONTH).getTime() > truncate(b, Calendar.DAY_OF_MONTH).getTime(); + } + + /** + * Returns {@code true} if date {@code a} is the same date as date {@code b} (ignoring the time component), else returns {@code false} + */ + public static boolean isSameDate(Date a, Date b) { + return truncate(a, Calendar.DAY_OF_MONTH).getTime() == truncate(b, Calendar.DAY_OF_MONTH).getTime(); + } + + /** + * Calculate the difference in days of (toDate - fromDate), ignoring the time component of both dates. + */ + public static long getDiffInDays(Date fromDate, Date toDate) { + // ignore the time component of both dates + Date fromDateT = truncate(fromDate, Calendar.DAY_OF_MONTH); + Date toDateT = truncate(toDate, Calendar.DAY_OF_MONTH); + + // calculate the difference in ms divided by MILLIS_PER_DAY, thus the difference in days + return (toDateT.getTime() - fromDateT.getTime()) / MILLIS_PER_DAY; + } + + /** + * Calculate the difference in minutes of (toDate - fromDate), by truncating up to the minute of both dates. + */ + public static final long getDiffInMins(Date fromDate, Date toDate) { + // truncate up to the minute of both dates + Date fromDateT = DateUtils.truncate(fromDate, Calendar.MINUTE); + Date toDateT = DateUtils.truncate(toDate, Calendar.MINUTE); + + return (toDateT.getTime() - fromDateT.getTime()) / DateUtils.MILLIS_PER_MINUTE; + } + + /** + * Calculate time diff in mins (same day or within 1 day) + */ + public static final int getDiffInMins(String timeFrom, String timeTo) { + int diffMins = 0; + + // defaults to "0000" if empty + if (StringUtils.isBlank(timeFrom)) timeFrom = "0000"; + if (StringUtils.isBlank(timeTo)) timeTo = "0000"; + + if (!isValid24HourTime(timeFrom) || !isValid24HourTime(timeTo)) { + return diffMins; + } + + try { + int timeFromHour = Integer.parseInt(timeFrom.substring(0, 2)); + int timeFromMin = Integer.parseInt(timeFrom.substring(2)); + int timeToHour = Integer.parseInt(timeTo.substring(0, 2)); + int timeToMin = Integer.parseInt(timeTo.substring(2)); + + diffMins += 60 - timeFromMin; + diffMins += timeToMin; + diffMins += ((timeToHour - 1) - timeFromHour) * 60; + + if (timeToHour < timeFromHour) { + // may be across a 00:00 + diffMins += 24 * 60; + } + } catch (Exception e) { + } + + return diffMins; + } + + public static final String get24HourTimeFromDate(Date date) { + if (date == null) return StringUtils.EMPTY; + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + return DF00.format(calendar.get(Calendar.HOUR_OF_DAY)) + DF00.format(calendar.get(Calendar.MINUTE)); + } + + public static final String get24HourTimeFromDateWithColon(Date date) { + if (date == null) return StringUtils.EMPTY; + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + return DF00.format(calendar.get(Calendar.HOUR_OF_DAY)) + ":" + DF00.format(calendar.get(Calendar.MINUTE)); + } + + /** + *

+ * Adds or subtracts the specified amount of time to the given calendar field to the {@code date} returning a new object (the original {@code date} object + * is unchanged), based on the calendar's rules. + *

+ *

+ * For example, to subtract 5 days from the {@code date}, you can achieve it by calling:
+ * {@code add(date, Calendar.DAY_OF_MONTH, -5)}. + *

+ * + * @param clazz + * the date class to return (supports {@link java.util.Date}, {@link java.sql.Date}, {@link java.sql.Timestamp}) + * @param date + * the {@code date}, not {@code null} + * @param calendarField + * the calendar field to add to (e.g. {@code Calendar.DAY_OF_MONTH}) + * @param amount + * the amount to add, may be negative + * + * @return the new {@code date} object with the amount added + * + * @throws IllegalArgumentException + * if the {@code date} is {@code null} + * + * @see Calendar#add(int, int) + */ + @SuppressWarnings("unchecked") + public static T add(Class clazz, Date date, int calendarField, int amount) { + if (date == null) { + throw new IllegalArgumentException("The date must not be null"); + } + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.add(calendarField, amount); + if (clazz == Date.class) { + return (T) c.getTime(); + } else if (clazz == java.sql.Date.class) { + return (T) new java.sql.Date(c.getTimeInMillis()); + } else if (clazz == Timestamp.class) { + return (T) new java.sql.Timestamp(c.getTimeInMillis()); + } else { + throw new UnsupportedOperationException(clazz.getName() + " is not supported."); + } + } + + /** + *

+ * Adds or subtracts the specified amount of years to the {@code date} returning a new object (the original {@code date} object is unchanged), based on the + * calendar's rules. + *

+ * + * @param clazz + * the date class to return (supports {@link java.util.Date}, {@link java.sql.Date}, {@link java.sql.Timestamp}) + * @param date + * the {@code date}, not {@code null} + * @param amount + * the amount to add, may be negative + * + * @return the new {@code date} object with the amount added + * + * @throws IllegalArgumentException + * if the {@code date} is {@code null} + * + * @see Calendar#add(int, int) + */ + public static T addYears(Class clazz, Date date, int amount) { + return add(clazz, date, Calendar.YEAR, amount); + } + + /** + *

+ * Adds or subtracts the specified amount of months to the {@code date} returning a new object (the original {@code date} object is unchanged), based on the + * calendar's rules. + *

+ * + * @param clazz + * the date class to return (supports {@link java.util.Date}, {@link java.sql.Date}, {@link java.sql.Timestamp}) + * @param date + * the {@code date}, not {@code null} + * @param amount + * the amount to add, may be negative + * + * @return the new {@code date} object with the amount added + * + * @throws IllegalArgumentException + * if the {@code date} is {@code null} + * + * @see Calendar#add(int, int) + */ + public static T addMonths(Class clazz, Date date, int amount) { + return add(clazz, date, Calendar.MONTH, amount); + } + + /** + *

+ * Adds or subtracts the specified amount of days to the {@code date} returning a new object (the original {@code date} object is unchanged), based on the + * calendar's rules. + *

+ * + * @param clazz + * the date class to return (supports {@link java.util.Date}, {@link java.sql.Date}, {@link java.sql.Timestamp}) + * @param date + * the {@code date}, not {@code null} + * @param amount + * the amount to add, may be negative + * + * @return the new {@code date} object with the amount added + * + * @throws IllegalArgumentException + * if the {@code date} is {@code null} + * + * @see Calendar#add(int, int) + */ + public static T addDays(Class clazz, Date date, int amount) { + return add(clazz, date, Calendar.DAY_OF_MONTH, amount); + } + + /** + *

+ * Adds or subtracts the specified amount of hours to the {@code date} returning a new object (the original {@code date} object is unchanged), based on the + * calendar's rules. + *

+ * + * @param clazz + * the date class to return (supports {@link java.util.Date}, {@link java.sql.Date}, {@link java.sql.Timestamp}) + * @param date + * the {@code date}, not {@code null} + * @param amount + * the amount to add, may be negative + * + * @return the new {@code date} object with the amount added + * + * @throws IllegalArgumentException + * if the {@code date} is {@code null} + * + * @see Calendar#add(int, int) + */ + public static T addHours(Class clazz, Date date, int amount) { + return add(clazz, date, Calendar.HOUR_OF_DAY, amount); + } + + /** + *

+ * Adds or subtracts the specified amount of minutes to the {@code date} returning a new object (the original {@code date} object is unchanged), based on + * the calendar's rules. + *

+ * + * @param clazz + * the date class to return (supports {@link java.util.Date}, {@link java.sql.Date}, {@link java.sql.Timestamp}) + * @param date + * the {@code date}, not {@code null} + * @param amount + * the amount to add, may be negative + * + * @return the new {@code date} object with the amount added + * + * @throws IllegalArgumentException + * if the {@code date} is {@code null} + * + * @see Calendar#add(int, int) + */ + public static T addMinutes(Class clazz, Date date, int amount) { + return add(clazz, date, Calendar.MINUTE, amount); + } + + /** + *

+ * Adds or subtracts the specified amount of seconds to the {@code date} returning a new object (the original {@code date} object is unchanged), based on + * the calendar's rules. + *

+ * + * @param clazz + * the date class to return (supports {@link java.util.Date}, {@link java.sql.Date}, {@link java.sql.Timestamp}) + * @param date + * the {@code date}, not {@code null} + * @param amount + * the amount to add, may be negative + * + * @return the new {@code date} object with the amount added + * + * @throws IllegalArgumentException + * if the {@code date} is {@code null} + * + * @see Calendar#add(int, int) + */ + public static T addSeconds(Class clazz, Date date, int amount) { + return add(clazz, date, Calendar.SECOND, amount); + } + + /** + * Returns the day of the month represented by this Date object. + * + * @return a value between {@code 1} and {@code 31} representing the day of the month + * + * @see Calendar#get(Calendar.DAY_OF_MONTH) + */ + public static int getDay(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + return c.get(Calendar.DAY_OF_MONTH); + } + + /** + * Returns the day of the week represented by this Date object. + * + * @return a value from {@code 1} (Sunday) to {@code 7} (Saturday) representing the day of the week + * + * @see Calendar#get(Calendar.DAY_OF_WEEK) + */ + public static int getDayOfWeek(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + return c.get(Calendar.DAY_OF_WEEK); + } + + /** + * Returns a number representing the month represented by this Date object + * + * @return a value between {@code 0} and {@code 11} representing the month, with the value {@code 0} representing January + * + * @see Calendar#get(Calendar.MONTH) + */ + public static int getMonth(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + return c.get(Calendar.MONTH); + } + + /** + * Returns the year represented by this Date object + * + * @see Calendar#get(Calendar.YEAR) + */ + public static int getYear(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + return c.get(Calendar.YEAR); + } + + /** + * Returns the number of days of the month represented by this Date object + * + * @param date + * non-{@code null} {@code date} object + */ + public static int getNumberOfDaysOfMonth(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + return c.getActualMaximum(Calendar.DAY_OF_MONTH); + } + + /** + * Returns the number of days by range represented by this Date object + * + * @param dateFrom + * Date Start From + * @param dateTo + * Date End To + */ + public static int getNumberOfDaysByRange(Date dateFrom, Date dateTo) { + dateFrom = DateUtils.truncate(dateFrom, Calendar.DAY_OF_MONTH); + dateTo = DateUtils.truncate(dateTo, Calendar.DAY_OF_MONTH); + + Calendar fromCal = Calendar.getInstance(); + fromCal.setTime(dateFrom); + + Calendar toCal = Calendar.getInstance(); + toCal.setTime(dateTo); + int count = 0; + while (DateUtils.isEarlierThan(fromCal.getTime(), toCal.getTime())) { + count++; + fromCal.add(Calendar.DAY_OF_MONTH, 1); + } + return count; + } + + /** + * Returns the first date of the month represented by this Date object + * + * @param date + * non-{@code null} {@code date} object + */ + public static Date getFirstDateOfMonth(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.DAY_OF_MONTH, 1); + return calendar.getTime(); + } + + /** + * Returns the last date of the month represented by this Date object + * + * @param date + * non-{@code null} {@code date} object + */ + public static Date getLastDateOfMonth(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH)); + return calendar.getTime(); + } + + /** + * Returns the first date of the last month represented by this Date object + * + * @param date + * non-{@code null} {@code date} object + */ + public static Date getFirstDateOfLastMonth(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.add(Calendar.MONTH, -1); + c.set(Calendar.DAY_OF_MONTH, 1); + return c.getTime(); + } + + /** + * Returns the last date of the last month represented by this Date object + * + * @param date + * non-{@code null} {@code date} object + */ + public static Date getLastDateOfLastMonth(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.add(Calendar.MONTH, -1); + c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH)); + return c.getTime(); + } + + /** + * Returns the first date of the year + * + * @param year + * the year + */ + public static Date getFirstDateOfYear(int year) { + Calendar calendar = Calendar.getInstance(); + calendar.set(year, Calendar.JANUARY, 1, 0, 0, 0); + return calendar.getTime(); + } + + /** + * Returns the last date of the year + * + * @param year + * the year + */ + public static Date getLastDateOfYear(int year) { + Calendar calendar = Calendar.getInstance(); + calendar.set(year, Calendar.DECEMBER, 31, 0, 0, 0); + return calendar.getTime(); + } + + /** + * Returns the first date of the week represented by this Date object + * + * @param date + * non-{@code null} {@code date} object + */ + public static Date getFirstDateOfWeek(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.add(Calendar.DAY_OF_MONTH, -c.get(Calendar.DAY_OF_WEEK) + 1); + return c.getTime(); + } + + /** + * Returns the first date of the week represented by this Date object, where you can specify the first day of the week + * + * @param date + * non-{@code null} {@code date} object + * @param firstDayOfWeek + * the first day of the week (e.g. Calendar.SUNDAY, Calendar.MONDAY, etc) + */ + public static Date getFirstDateOfWeek(Date date, int firstDayOfWeek) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + int diff = c.get(Calendar.DAY_OF_WEEK) - firstDayOfWeek; + if (diff < 0) diff += 7; + c.add(Calendar.DAY_OF_MONTH, -diff); + return c.getTime(); + } + + /** + * Returns the last date of the week represented by this Date object + * + * @param date + * non-{@code null} {@code date} object + */ + public static Date getLastDateOfWeek(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.add(Calendar.DAY_OF_MONTH, 7 - c.get(Calendar.DAY_OF_WEEK)); + return c.getTime(); + } + + /** + * Returns the last date of the week represented by this Date object, where you can specify the first day of the week + * + * @param date + * non-{@code null} {@code date} object + * @param firstDayOfWeek + * the first day of the week (e.g. Calendar.SUNDAY, Calendar.MONDAY, etc) + */ + public static Date getLastDateOfWeek(Date date, int firstDayOfWeek) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + int diff = c.get(Calendar.DAY_OF_WEEK) - firstDayOfWeek; + if (diff < 0) diff += 7; + c.add(Calendar.DAY_OF_MONTH, 6 - diff); + return c.getTime(); + } + + /** + * Returns the first date of month by year and month + * + * @param year + * the year + * @param month + * a value between {@code 1} and {@code 12} representing January to December + */ + public static Date getFirstDateOfMonthByYearAndMonth(int year, int month) { + Calendar c = Calendar.getInstance(); + c.set(year, month - 1, 1, 0, 0, 0); + return c.getTime(); + } + + /** + * Returns the last date of month by year and month + * + * @param year + * the year + * @param month + * a value between {@code 1} and {@code 12} representing January to December + */ + public static Date getLastDateOfMonthByYearAndMonth(int year, int month) { + Calendar c = Calendar.getInstance(); + c.set(year, month - 1, 1, 0, 0, 0); + c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH)); + return c.getTime(); + } + + /** + * Returns a new {@link java.sql.Date} object from the value of the {@link java.util.Date} object, returns {@code null} if {@code date} is null + * + * @param date + * {@code date} object, may be {@code null} + */ + public static java.sql.Date toSqlDate(Date date) { + if (date == null) return null; + return new java.sql.Date(date.getTime()); + } + + /** + * Returns a new {@link java.sql.Timestamp} object from the value of the {@link java.util.Date} object, returns {@code null} if {@code date} is null + * + * @param date + * {@code date} object, may be {@code null} + */ + public static java.sql.Timestamp toTimestamp(Date date) { + if (date == null) return null; + return new java.sql.Timestamp(date.getTime()); + } + + /** + * Returns the number of days in a month count by the Day of Week (Sunday to Saturday) + * + * @param year + * 4-digit year + * @param month + * 1 to 12 (Jan to Dec) + * @param weekDays + * Calendar.SUNDAY to Calendar.SATURDAY + * + * @return the number of days in a month count by the Day of Week + */ + public static int countDaysByDayOfWeek(int year, int month, int... dayOfWeeks) { + Calendar c = Calendar.getInstance(); + c.clear(); + c.set(year, month - 1, 1); // first day of month + + int dayOfWeek = c.get(Calendar.DAY_OF_WEEK); + int daysInMonth = c.getActualMaximum(Calendar.DAY_OF_MONTH); + + int count = 0; + for (int day = 1; day <= daysInMonth; day++) { + for (int dow : dayOfWeeks) { + if (dayOfWeek == dow) count++; + } + dayOfWeek++; + if (dayOfWeek > Calendar.SATURDAY) dayOfWeek = Calendar.SUNDAY; + } + return count; + } + + /** + * Returns the number of days in a month count by the Day of Week (Sunday to Saturday) + * + * @param dateFrom + * Date Start From + * @param dateTo + * Date End To + * @param weekDays + * Calendar.SUNDAY to Calendar.SATURDAY + * + * @return the number of days in a month count by the Day of Week + */ + public static int countDaysByDayOfWeek(Date dateFrom, Date dateTo, int... dayOfWeeks) { + dateFrom = DateUtils.truncate(dateFrom, Calendar.DAY_OF_MONTH); + dateTo = DateUtils.truncate(dateTo, Calendar.DAY_OF_MONTH); + + Calendar fromCal = Calendar.getInstance(); + fromCal.setTime(dateFrom); + + Calendar toCal = Calendar.getInstance(); + toCal.setTime(dateTo); + + int count = 0; + while (DateUtils.isEarlierThan(fromCal.getTime(), toCal.getTime())) { + int dayOfWeek = fromCal.get(Calendar.DAY_OF_WEEK); + for (int dow : dayOfWeeks) { + if (dayOfWeek == dow) { + count++; + break; + } + } + fromCal.add(Calendar.DAY_OF_MONTH, 1); + } + return count; + } + + /** + * Returns {@code true} if date {@code a} is the same month and year as date {@code b} (ignoring the time component), else returns {@code false} + */ + public static boolean isSameMonthYear(Date a, Date b) { + return getYear(a) == getYear(b) && getMonth(a) == getMonth(b); + } + + /** + * Returns {@code true} if date {@code a} is the same week and year as date {@code b} (ignoring the time component), else returns {@code false} + */ + public static boolean isSameWeekYear(Date a, Date b) { + return getYear(a) == getYear(b) && getWeekOfYear(a) == getWeekOfYear(b); + } + + /** + * new Date() is deprecated + * + * @param year + * full year + * @param month + * 0-based + * @param date + * 1-based + * @return java.util.Date + */ + public static Date newDate(int year, int month, int date) { + Calendar instance = Calendar.getInstance(); + instance.clear(); + instance.set(year, month, date); + return instance.getTime(); + } + + /** + * new Date() is deprecated + * + * @param year + * full year + * @param month + * 0-based + * @param date + * 1-based + * @return java.util.Date + */ + public static Date newDate(int year, int month, int date, int hourOfDay, int minute) { + Calendar instance = Calendar.getInstance(); + instance.clear(); + instance.set(year, month, date, hourOfDay, minute); + return instance.getTime(); + } + + /** + * new Date() is deprecated + * + * @param year + * full year + * @param month + * 0-based + * @param date + * 1-based + * @return java.util.Date + */ + public static Date newDate(int year, int month, int date, int hourOfDay, int minute, int second) { + Calendar instance = Calendar.getInstance(); + instance.clear(); + instance.set(year, month, date, hourOfDay, minute, second); + return instance.getTime(); + } +} diff --git a/src/main/java/com/ffii/core/utils/FileUtils.java b/src/main/java/com/ffii/core/utils/FileUtils.java new file mode 100644 index 0000000..38f94c7 --- /dev/null +++ b/src/main/java/com/ffii/core/utils/FileUtils.java @@ -0,0 +1,111 @@ +package com.ffii.core.utils; + +import java.util.HashMap; +import java.util.Map; + +/** + * File Utils + * + * @author Patrick + */ +public abstract class FileUtils { + + private static final Map MIMETYPES = new HashMap<>(); + + static { + MIMETYPES.put("pdf", "application/pdf"); + + MIMETYPES.put("doc", "application/msword"); + MIMETYPES.put("dot", "application/msword"); + MIMETYPES.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + + MIMETYPES.put("xls", "application/vnd.ms-excel"); + MIMETYPES.put("xlm", "application/vnd.ms-excel"); + MIMETYPES.put("xla", "application/vnd.ms-excel"); + MIMETYPES.put("xlc", "application/vnd.ms-excel"); + MIMETYPES.put("xlt", "application/vnd.ms-excel"); + MIMETYPES.put("xlw", "application/vnd.ms-excel"); + MIMETYPES.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + + MIMETYPES.put("ppt", "application/vnd.ms-powerpoint"); + MIMETYPES.put("pps", "application/vnd.ms-powerpoint"); + MIMETYPES.put("pot", "application/vnd.ms-powerpoint"); + MIMETYPES.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"); + + MIMETYPES.put("bat", "application/x-msdownload"); + MIMETYPES.put("com", "application/x-msdownload"); + MIMETYPES.put("dll", "application/x-msdownload"); + MIMETYPES.put("exe", "application/x-msdownload"); + MIMETYPES.put("msi", "application/x-msdownload"); + + MIMETYPES.put("swf", "application/x-shockwave-flash"); + + MIMETYPES.put("7z", "application/x-7z-compressed"); + MIMETYPES.put("rar", "application/x-rar-compressed"); + MIMETYPES.put("zip", "application/zip"); + + MIMETYPES.put("js", "application/javascript"); + MIMETYPES.put("json", "application/json"); + + MIMETYPES.put("mpga", "audio/mpeg"); + MIMETYPES.put("mp2", "audio/mpeg"); + MIMETYPES.put("mp2a", "audio/mpeg"); + MIMETYPES.put("mp3", "audio/mpeg"); + MIMETYPES.put("m2a", "audio/mpeg"); + MIMETYPES.put("m3a", "audio/mpeg"); + + MIMETYPES.put("bmp", "image/bmp"); + MIMETYPES.put("gif", "image/gif"); + MIMETYPES.put("jpeg", "image/jpeg"); + MIMETYPES.put("jpg", "image/jpeg"); + MIMETYPES.put("jpe", "image/jpeg"); + MIMETYPES.put("png", "image/png"); + MIMETYPES.put("tiff", "image/tiff"); + MIMETYPES.put("tif", "image/tiff"); + + MIMETYPES.put("css", "text/css"); + + MIMETYPES.put("csv", "text/csv"); + + MIMETYPES.put("html", "text/html"); + MIMETYPES.put("htm", "text/html"); + + MIMETYPES.put("txt", "text/plain"); + MIMETYPES.put("text", "text/plain"); + MIMETYPES.put("conf", "text/plain"); + MIMETYPES.put("log", "text/plain"); + + MIMETYPES.put("mp4", "video/mp4"); + MIMETYPES.put("mp4v", "video/mp4"); + MIMETYPES.put("mpg4", "video/mp4"); + + MIMETYPES.put("mpeg", "video/mpeg"); + MIMETYPES.put("mpg", "video/mpeg"); + MIMETYPES.put("mpe", "video/mpeg"); + MIMETYPES.put("m1v", "video/mpeg"); + MIMETYPES.put("m2v", "video/mpeg"); + + MIMETYPES.put("qt", "video/quicktime"); + MIMETYPES.put("mov", "video/quicktime"); + + MIMETYPES.put("wmv", "video/x-ms-wmv"); + MIMETYPES.put("wmx", "video/x-ms-wmx"); + MIMETYPES.put("wvx", "video/x-ms-wvx"); + MIMETYPES.put("avi", "video/x-msvideo"); + + // MIMETYPES.put("xxxxx", "xxxxx"); + } + + /** + * Guess the mimetype from the file name extension + * + * @return The mimetype guessed from the file name extension, or {@code null} if the mimetype cannot be determined + */ + public static String guessMimetype(String filename) { + String extension = StringUtils.substringAfterLast(filename, "."); + String mimetype = MIMETYPES.get(extension); + return mimetype != null ? mimetype : "application/octet-stream"; + } + +} + diff --git a/src/main/java/com/ffii/core/utils/NumberUtils.java b/src/main/java/com/ffii/core/utils/NumberUtils.java new file mode 100644 index 0000000..4a2d8b3 --- /dev/null +++ b/src/main/java/com/ffii/core/utils/NumberUtils.java @@ -0,0 +1,461 @@ +package com.ffii.core.utils; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import org.apache.commons.lang3.ArrayUtils; + +/** + * NumberUtils extends from Apache Commons, and incl some methods from MathUtils + * + * @author Patrick + */ +public abstract class NumberUtils extends org.apache.commons.lang3.math.NumberUtils { + + /** + * Round the given value to the specified number of decimal places. The value is rounded using the given method which is any method defined in + * {@link BigDecimal}. + * + * @param x + * the value to round + * @param scale + * the number of digits to the right of the decimal point + * @param roundingMethod + * the rounding method as defined in {@link RoundingMode} + * @return the rounded value + */ + public static double round(double x, int scale, RoundingMode roundingMethod) { + try { + return BigDecimal.valueOf(x).setScale(scale, roundingMethod).doubleValue(); + } catch (NumberFormatException ex) { + if (Double.isInfinite(x)) { + return x; + } else { + return Double.NaN; + } + } + } + + /** + * Round the given value to the specified number of decimal places. The value is rounded using the {@link RoundingMode#HALF_UP} method. + * + * @param x + * the value to round + * @param scale + * the number of digits to the right of the decimal point + * @return the rounded value + * @see org.apache.commons.math.util.MathUtils#round(double, int) + */ + public static double round(double x, int scale) { + return round(x, scale, RoundingMode.HALF_UP); + } + + /** + * Round up the given value to the specified number of decimal places. The value is rounded up using the {@link RoundingMode#UP} method. + * + * @param x + * the value to round up + * @param scale + * the number of digits to the right of the decimal point + * @return the rounded up value + */ + public static double roundUp(double x, int scale) { + return round(x, scale, RoundingMode.UP); + } + + /** + * Round down the given value to the specified number of decimal places. The value is rounded down using the {@link RoundingMode#DOWN} method. + * + * @param x + * the value to round down + * @param scale + * the number of digits to the right of the decimal point + * @return the rounded down value + */ + public static double roundDown(double x, int scale) { + return round(x, scale, RoundingMode.DOWN); + } + + /** + * Return the {@code int} value of an {@code Object}, or the default value if the object is either {@code null} or not an instance of {@code Number}. + * + * @param obj + * the {@code Object} + * @param defaultValue + * the default value + * @return the {@code int} value of the {@code Object}, or the default if the object is either {@code null} or not an instance of {@code Number} + * @see Number#intValue() + */ + public static int intValue(Object obj, int defaultValue) { + if (obj instanceof Number) { + return ((Number) obj).intValue(); + } else if (obj instanceof String) { + try { + return Integer.parseInt((String) obj); + } catch (NumberFormatException nfe) { + } + } + return defaultValue; + } + + /** + * Return the {@code int} value of an {@code Object}, or {@code zero} if the object is either {@code null} or not an instance of {@code Number}. + * + * @param obj + * the {@code Object} + * @return the {@code int} value of the {@code Object}, or {@code zero} if the object is either {@code null} or not an instance of {@code Number} + * @see Number#intValue() + */ + public static int intValue(Object obj) { + return intValue(obj, 0); + } + + /** + * Convert an {@code Object} to an {@code Integer} (only if the object is an instance of {@code Number} or {@code String}), returning a default value if the + * conversion fails. + *

+ * If the object is {@code null}, the default value is returned. + * + * @param obj + * the object to convert, may be {@code null} + * @param defaultValue + * the default value, may be {@code null} + * @return the Integer represented by the object, or the default if conversion fails + */ + public static Integer toInt(Object obj, Integer defaultValue) { + if (obj instanceof Number) + return Integer.valueOf(((Number) obj).intValue()); + else if (obj instanceof String) + try { + return Integer.valueOf((String) obj); + } catch (NumberFormatException nfe) { + return defaultValue; + } + else + return defaultValue; + } + + /** + * Return the {@code long} value of a {@code Long} object, or a default value if the object is {@code null}. + * + * @param obj + * the object (can be {@code null}) + * @param defaultValue + * the default value + * @return the {@code long} value of the object if it's a number, or the default value if the object is {@code null} or {@code NaN} + * @see Long#longValue() + */ + public static long longValue(Object obj, long defaultValue) { + return (obj instanceof Number) ? ((Number) obj).longValue() : defaultValue; + } + + /** + * Return the {@code long} value of a {@code Long} object, or {@code zero} if the object is {@code null}. + * + * @param obj + * the object (can be {@code null}) + * @return the {@code long} value of the object if it's a number, or {@code zero} if the object is {@code null} or {@code NaN} + * @see Long#longValue() + */ + public static long longValue(Object obj) { + return longValue(obj, 0l); + } + + /** + * Convert an {@code Object} to a {@code Long} (only if the object is an instance of {@code Number} or {@code String}), returning a default value if the + * conversion fails. + *

+ * If the object is {@code null}, the default value is returned. + * + * @param obj + * the object to convert, may be {@code null} + * @param defaultValue + * the default value, may be {@code null} + * @return the Long represented by the object, or the default if conversion fails + */ + public static Long toLong(Object obj, Long defaultValue) { + if (obj instanceof Number) + return Long.valueOf(((Number) obj).longValue()); + else if (obj instanceof String) + try { + return Long.valueOf((String) obj); + } catch (NumberFormatException nfe) { + return defaultValue; + } + else + return defaultValue; + } + + /** + * @param obj + * the object (can be {@code null}) + * @param defaultValue + * the default value + * @return the {@code double} value of the object if it's a number, or the default value if the object is {@code null} or {@code NaN} + * @see Double#doubleValue() + */ + public static double doubleValue(Object obj, double defaultValue) { + return (obj instanceof Number) ? ((Number) obj).doubleValue() : defaultValue; + } + + /** + * @param obj + * the object (can be {@code null}) + * @param defaultValue + * the default value + * @return the {@code double} value of the object if it's a number, or {@code zero} if the object is {@code null} or {@code NaN} + * @see Double#doubleValue() + */ + public static double doubleValue(Object obj) { + return doubleValue(obj, 0.0d); + } + + /** + * Convert an {@code Object} to a {@code Double} (only if the object is an instance of {@code Number} or {@code String}), returning a default value if the + * conversion fails. + *

+ * If the object is {@code null}, the default value is returned. + * + * @param obj + * the object to convert, may be {@code null} + * @param defaultValue + * the default value, may be {@code null} + * @return the Double represented by the object, or the default if conversion fails + */ + public static Double toDouble(Object obj, Double defaultValue) { + if (obj instanceof Number) + return Double.valueOf(((Number) obj).doubleValue()); + else if (obj instanceof String) + try { + return Double.valueOf((String) obj); + } catch (NumberFormatException nfe) { + return defaultValue; + } + else + return defaultValue; + } + + /** + * Return the {@code BigDecimal} object, or {@code zero} if the object is {@code null}. + * + * @param obj + * the {@code BigDecimal} object + * @return the {@code BigDecimal} object, or {@code zero} if the object is {@code null} + */ + public static BigDecimal decimalValue(BigDecimal obj) { + return decimalValue(obj, BigDecimal.ZERO); + } + + /** + * Return the {@code BigDecimal} object, or a default value if the object is {@code null}. + * + * @param obj + * the {@code BigDecimal} object + * @param defaultValue + * the default value + * @return the {@code BigDecimal} object, or the default if the object is {@code null} + */ + public static BigDecimal decimalValue(BigDecimal obj, BigDecimal defaultValue) { + return obj == null ? defaultValue : obj; + } + + /** + * Convert an {@code Object} to a {@code BigDecimal}, returning {@code BigDecimal.ZERO} if the conversion fails (e.g. the object is not an instance of + * {@code Number} nor {@code String}). + *

+ * If the object is {@code null}, {@code BigDecimal.ZERO} is returned. + * + * @param obj + * the object to convert, may be {@code null} + * @return the BigDecimal represented by the object, or {@code BigDecimal.ZERO} if conversion fails + */ + public static BigDecimal toDecimal(Object obj) { + return toDecimal(obj, BigDecimal.ZERO); + } + + /** + * Convert an {@code Object} to a {@code BigDecimal}, returning a default value if the conversion fails (e.g. the object is not an instance of + * {@code Number} nor {@code String}). + *

+ * If the object is {@code null}, the default value is returned. + * + * @param obj + * the object to convert, may be {@code null} + * @param defaultValue + * the default value, may be {@code null} + * @return the BigDecimal represented by the object, or the default if conversion fails + */ + public static BigDecimal toDecimal(Object obj, BigDecimal defaultValue) { + if (obj instanceof BigDecimal) + return (BigDecimal) obj; + else if (obj instanceof Number) + return BigDecimal.valueOf(((Number) obj).doubleValue()); + else if (obj instanceof String) + try { + return new BigDecimal((String) obj); + } catch (NumberFormatException nfe) { + return defaultValue; + } + else + return defaultValue; + } + + /** + * Null-safe method to check if the two {@code Integer} objects have the same value. + * + *

    + *
  1. Returns {@code true} if {@code a} and {@code b} are both {@code null}. + *
  2. Returns {@code false} if only one of them is {@code null}. + *
  3. Returns {@code true} if {@code a} and {@code b} are not {@code null} and have the same {@code int} value, else returns {@code false}. + *
+ * + * @param a + * Integer obj, may be {@code null} + * @param b + * Integer obj, may be {@code null} + */ + public static boolean isEqual(Integer a, Integer b) { + return a == null ? (b == null ? true : false) : (b == null ? false : a.equals(b)); + } + + /** + * Null-safe method to check if the two {@code Integer} objects have different values. + * + *
    + *
  1. Returns {@code false} if {@code a} and {@code b} are both {@code null}. + *
  2. Returns {@code true} if only one of them is {@code null}. + *
  3. Returns {@code true} if {@code a} and {@code b} are not {@code null} and have different {@code int} values, else returns {@code false}. + *
+ * + * @param a + * Integer obj, may be {@code null} + * @param b + * Integer obj, may be {@code null} + */ + public static boolean isNotEqual(Integer a, Integer b) { + return !isEqual(a, b); + } + + /** + * Null-safe method to check if the two {@code BigDecimal} objects have the same value. + *

+ * Two {@code BigDecimal} objects that are equal in value but have a different scale (like 2.0 and 2.00) are considered equal by this method. + * + *

    + *
  1. Returns {@code true} if {@code a} and {@code b} are both {@code null}. + *
  2. Returns {@code false} if only one of them is {@code null}. + *
  3. Returns {@code true} if {@code a} and {@code b} are not {@code null} and have the same {@code decimal} value, else returns {@code false}. + *
+ * + * @param a + * BigDecimal obj, may be {@code null} + * @param b + * BigDecimal obj, may be {@code null} + */ + public static boolean isEqual(BigDecimal a, BigDecimal b) { + return a == null ? (b == null ? true : false) : (b == null ? false : a.compareTo(b) == 0); + } + + /** + * Null-safe method to check if the two {@code BigDecimal} objects have different values. + *

+ * Two {@code BigDecimal} objects that are equal in value but have a different scale (like 2.0 and 2.00) are considered equal by this method. + * + *

    + *
  1. Returns {@code false} if {@code a} and {@code b} are both {@code null}. + *
  2. Returns {@code true} if only one of them is {@code null}. + *
  3. Returns {@code true} if {@code a} and {@code b} are not {@code null} and have different {@code decimal} values, else returns {@code false}. + *
+ * + * @param a + * BigDecimal obj, may be {@code null} + * @param b + * BigDecimal obj, may be {@code null} + */ + public static boolean isNotEqual(BigDecimal a, BigDecimal b) { + return !isEqual(a, b); + } + + /** + * Check if {@code BigDecimal} object {@code a} is greater than {@code BigDecimal} object {@code b}. + * + * @param a + * non-{@code null} BigDecimal obj + * @param b + * non-{@code null} BigDecimal obj + */ + public static boolean isGreaterThan(BigDecimal a, BigDecimal b) { + return a.compareTo(b) > 0; + } + + /** + * Check if {@code BigDecimal} object {@code a} is greater than or equals to {@code BigDecimal} object {@code b}. + * + * @param a + * non-{@code null} BigDecimal obj + * @param b + * non-{@code null} BigDecimal obj + */ + public static boolean isGreaterThanOrEqual(BigDecimal a, BigDecimal b) { + return a.compareTo(b) >= 0; + } + + /** + * Check if {@code BigDecimal} object {@code a} is less than {@code BigDecimal} object {@code b}. + * + * @param a + * non-{@code null} BigDecimal obj + * @param b + * non-{@code null} BigDecimal obj + */ + public static boolean isLessThan(BigDecimal a, BigDecimal b) { + return a.compareTo(b) < 0; + } + + /** + * Check if {@code BigDecimal} object {@code a} is less than or equals to {@code BigDecimal} object {@code b}. + * + * @param a + * non-{@code null} BigDecimal obj + * @param b + * non-{@code null} BigDecimal obj + */ + public static boolean isLessThanOrEqual(BigDecimal a, BigDecimal b) { + return a.compareTo(b) <= 0; + } + + /** + * + *
+	 * NumberUtils.equalsAny(null, (Integer[]) null) = false
+	 * NumberUtils.equalsAny(null, null, null)       = true
+	 * NumberUtils.equalsAny(null, 1, 2)             = false
+	 * NumberUtils.equalsAny(1, null, 2)             = false
+	 * NumberUtils.equalsAny(1, 1, 2)                = true
+	 * 
+ * + * @param int + * to compare, may be {@code null}. + * @param searchInts + * a int, may be {@code null}. + * @return {@code true} if the num is equal to any other element of searchInts; {@code false} if searchInts is null or contains no + * matches. + */ + public static boolean equalsAny(final int num, int... searchInts) { + if (ArrayUtils.isNotEmpty(searchInts)) { + for (int next : searchInts) { + if (num == next) { + return true; + } + } + } + return false; + } + + public static double sum(double... nums) { + BigDecimal rs = BigDecimal.ZERO; + for (double num : nums) + rs = rs.add(BigDecimal.valueOf(num)); + return rs.doubleValue(); + } +} diff --git a/src/main/java/com/ffii/core/utils/StringUtils.java b/src/main/java/com/ffii/core/utils/StringUtils.java new file mode 100644 index 0000000..454d865 --- /dev/null +++ b/src/main/java/com/ffii/core/utils/StringUtils.java @@ -0,0 +1,73 @@ +package com.ffii.core.utils; + +/** + * String Utils based on Apache Commons StringUtils. + * + * @author Patrick + */ +public abstract class StringUtils extends org.apache.commons.lang3.StringUtils { + + /** + * The String {@code "0"}. + */ + public static final String ZERO = "0"; + + /** + * The String {@code "1"}. + */ + public static final String ONE = "1"; + + /** + * The String {@code "%"}. + */ + public static final String PERCENT = "%"; + + /** + * The String {@code ","}. + */ + public static final String COMMA = ","; + + /** + * The String {@code "\r\n"} for line break on Windows + */ + public static final String LINE_BREAK_WINDOWS = "\r\n"; + + /** + * The String {@code "\n"} for line break on Unix/Linux + */ + public static final String LINE_BREAK_LINUX = "\n"; + + public static final String[] A2Z_LOWWER = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", + "o", "p", "q", "r", "s", "t", "u", "v", + "w", "x", "y", "z" }; + public static final String[] A2Z_UPPER = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", + "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z" }; + + public static final String concat(String segregator, final String... chars) { + if (segregator == null) + segregator = ""; + String rs = ""; + for (String c : chars) { + if (c == null) + continue; + else { + if (StringUtils.isBlank(rs)) { + rs = c; + } else { + rs += segregator + c; + } + } + } + return rs; + } + + public static final String removeLineBreak(String str) { + if (str == null) + return str; + return str.replace("\r\n", " ") + .replace("\n", " ") + .replace("\r", " ") + .trim(); + } +} diff --git a/src/main/java/com/ffii/core/view/AbstractView.java b/src/main/java/com/ffii/core/view/AbstractView.java new file mode 100644 index 0000000..6a3afbb --- /dev/null +++ b/src/main/java/com/ffii/core/view/AbstractView.java @@ -0,0 +1,29 @@ +package com.ffii.core.view; + +public abstract class AbstractView extends org.springframework.web.servlet.view.AbstractView { + + public static final String CONTENT_TYPE_JSON = "application/json"; + + public static final String CONTENT_TYPE_JAVASCRIPT = "text/javascript"; + + public static final String CONTENT_TYPE_TEXT_HTML = "text/html"; + public static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain"; + public static final String CONTENT_TYPE_XML = "text/xml"; + + public static final String CONTENT_TYPE_JPEG = "image/jpeg"; + public static final String CONTENT_TYPE_PNG = "image/png"; + + public static final String CHARSET_UTF8 = "UTF-8"; + + protected boolean disableCaching = true; + + /** + * Disables caching of the generated JSON.
+ * Default is {@code true}, which will prevent the client from caching the generated JSON. + */ + public void setDisableCaching(boolean disableCaching) { + this.disableCaching = disableCaching; + } + +} + diff --git a/src/main/java/com/ffii/tsms/modules/claim/entity/Claim.kt b/src/main/java/com/ffii/tsms/modules/claim/entity/Claim.kt new file mode 100644 index 0000000..ead1927 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/claim/entity/Claim.kt @@ -0,0 +1,46 @@ +package com.ffii.tsms.modules.claim.entity + +import com.ffii.core.entity.BaseEntity +import com.ffii.core.entity.IdEntity +import com.ffii.tsms.modules.data.entity.Staff +import com.ffii.tsms.modules.file.entity.File +import com.ffii.tsms.modules.project.entity.Project +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import java.math.BigDecimal +import java.time.LocalDateTime + +@Entity +@Table(name = "claim") +open class Claim : BaseEntity() { + + @NotNull + @ManyToOne + @JoinColumn(name = "staffId") + open var staff: Staff? = null + + @Column(name = "code") + open var code: String? = null + + @NotNull + @Column(name = "type") + open var type: String? = null + + @NotNull + @Column(name = "status") + open var status: String? = null + + @Column(name = "verifiedDatetime") + open var verifiedDatetime: LocalDateTime? = null + + @Column(name = "verifiedBy") + open var verifiedBy: Long? = null + + @Column(name = "remark", length = 255) + open var remark: String? = null +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetail.kt b/src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetail.kt new file mode 100644 index 0000000..14a921a --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetail.kt @@ -0,0 +1,47 @@ +package com.ffii.tsms.modules.claim.entity + +import com.ffii.core.entity.BaseEntity +import com.ffii.tsms.modules.data.entity.Staff +import com.ffii.tsms.modules.file.entity.File +import com.ffii.tsms.modules.project.entity.Project +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime + +@Entity +@Table(name = "claim_detail") +open class ClaimDetail : BaseEntity() { + + @NotNull + @ManyToOne + @JoinColumn(name = "claimId") + open var claim: Claim? = null + + @NotNull + @Column(name = "invoiceDate") + open var invoiceDate: LocalDate? = null + + @NotNull + @ManyToOne + @JoinColumn(name = "projectId") + open var project: Project? = null + + @NotNull + @Column(name = "description", length = 255) + open var description: String? = null + + @NotNull + @Column(name = "amount") + open var amount: BigDecimal? = null + + @OneToOne + @JoinColumn(name = "fileId") + open var file: File? = null +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetailRepository.kt b/src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetailRepository.kt new file mode 100644 index 0000000..3dc77d2 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetailRepository.kt @@ -0,0 +1,7 @@ +package com.ffii.tsms.modules.claim.entity + +import com.ffii.core.support.AbstractRepository + +interface ClaimDetailRepository : AbstractRepository { + fun findAllByDeletedFalse(): List +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/claim/entity/ClaimRepository.kt b/src/main/java/com/ffii/tsms/modules/claim/entity/ClaimRepository.kt new file mode 100644 index 0000000..66d2654 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/claim/entity/ClaimRepository.kt @@ -0,0 +1,9 @@ +package com.ffii.tsms.modules.claim.entity + +import com.ffii.core.support.AbstractRepository +import org.springframework.data.repository.query.Param + +interface ClaimRepository : AbstractRepository { + fun findAllByDeletedFalse(): List + fun findAllByCodeContains(@Param("code") code:String): List +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/claim/service/ClaimService.kt b/src/main/java/com/ffii/tsms/modules/claim/service/ClaimService.kt new file mode 100644 index 0000000..3103e7b --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/claim/service/ClaimService.kt @@ -0,0 +1,92 @@ +package com.ffii.tsms.modules.claim.service + +import com.ffii.tsms.modules.claim.entity.Claim +import com.ffii.tsms.modules.claim.entity.ClaimDetail +import com.ffii.tsms.modules.claim.entity.ClaimDetailRepository +import com.ffii.tsms.modules.claim.entity.ClaimRepository +import com.ffii.tsms.modules.claim.web.models.SaveClaimDetailRequest +import com.ffii.tsms.modules.claim.web.models.SaveClaimRequest +import com.ffii.tsms.modules.claim.web.models.SaveClaimResponse +import com.ffii.tsms.modules.file.entity.FileBlob +import com.ffii.tsms.modules.file.service.FileService +import com.ffii.tsms.modules.project.entity.ProjectRepository +import org.springframework.beans.BeanUtils +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.text.NumberFormat +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.HashMap + +@Service +open class ClaimService( + private val claimRepository: ClaimRepository, + private val fileService: FileService, private val claimDetailRepository: ClaimDetailRepository, private val projectRepository: ProjectRepository +) { + open fun allClaims(): List { + return claimRepository.findAllByDeletedFalse() + } + + open fun allClaimsByCodeContains(code: String): List { + return claimRepository.findAllByCodeContains(code) + } + + open fun generateCode(): String { + val dateFormatter = DateTimeFormatter.ofPattern("yyMMdd") + val formattedToday = LocalDate.now().format(dateFormatter) + val claims = allClaimsByCodeContains(formattedToday) + + // Format: CF-yymmddXXX + return "CF-" + formattedToday + String.format("%03d", (claims.size + 1)) + } + + @Transactional(rollbackFor = [Exception::class]) + open fun saveClaimDetail(claimDetail: SaveClaimDetailRequest, claim: Claim) { + val oldSupportingDocumentId = claimDetail.oldSupportingDocument?.id // fileId + val newSupportingDocument = claimDetail.newSupportingDocument + val claimDetailId = claimDetail.id + val projectId = claimDetail.project.id + var result: MutableMap = HashMap() + + if (oldSupportingDocumentId != null && oldSupportingDocumentId > 0 && newSupportingDocument != null) { + result = fileService.updateFile(multipartFile = newSupportingDocument, refId = claim.id, refType = "claimDetail", refCode = claim.code, maxSize = 50, fileId = oldSupportingDocumentId) + } else if ((oldSupportingDocumentId == null || oldSupportingDocumentId <= 0) && newSupportingDocument != null) { + result = fileService.uploadFile(multipartFile = newSupportingDocument, refId = claim.id, refType = "claimDetail", refCode = claim.code, maxSize = 50) + } + + val instance = claimDetailRepository.findById(claimDetailId).orElse(ClaimDetail()) + BeanUtils.copyProperties(claimDetail, instance) + + val project = projectRepository.findById(projectId!!).orElseThrow() + instance.apply { + this.project = project + } + + claimDetailRepository.save(instance) + } + + @Transactional(rollbackFor = [Exception::class]) + open fun saveClaim(saveClaimRequest: SaveClaimRequest): SaveClaimResponse { + + val claimId = saveClaimRequest.id + val addClaimDetails = saveClaimRequest.addClaimDetails + // Save to claim + val instance = if (claimId != null && claimId > 0) claimRepository.findById(claimId).orElseThrow() else Claim(); + + instance.apply { + type = saveClaimRequest.expenseType + code = if(instance.code.isNullOrEmpty()) generateCode() else instance.code + } + + claimRepository.save(instance) + + // Save to claim detail + if (addClaimDetails.isNotEmpty()) { + for(addClaimDetail in addClaimDetails) { + saveClaimDetail(addClaimDetail, instance) + } + } + + return SaveClaimResponse(claim = instance, message = "Success"); + } +} diff --git a/src/main/java/com/ffii/tsms/modules/claim/web/ClaimController.kt b/src/main/java/com/ffii/tsms/modules/claim/web/ClaimController.kt new file mode 100644 index 0000000..8a2c0fe --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/claim/web/ClaimController.kt @@ -0,0 +1,29 @@ +package com.ffii.tsms.modules.claim.web + +import com.ffii.tsms.modules.claim.entity.Claim +import com.ffii.tsms.modules.claim.service.ClaimService +import com.ffii.tsms.modules.claim.web.models.SaveClaimRequest +import com.ffii.tsms.modules.claim.web.models.SaveClaimResponse +import com.ffii.tsms.modules.data.web.models.SaveCustomerResponse +import com.ffii.tsms.modules.project.web.models.SaveCustomerRequest +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/claim") +class ClaimController(private val claimService: ClaimService) { + + @GetMapping + fun allClaims(): List { + return claimService.allClaims() + } + + @PostMapping("/save") + fun saveClaim(@Valid @RequestBody saveClaimRequest: SaveClaimRequest): SaveClaimResponse { + return claimService.saveClaim(saveClaimRequest) + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimRequest.kt b/src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimRequest.kt new file mode 100644 index 0000000..604c58c --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimRequest.kt @@ -0,0 +1,36 @@ +package com.ffii.tsms.modules.claim.web.models + +import com.ffii.tsms.modules.claim.entity.ClaimDetail +import jakarta.validation.constraints.NotBlank +import org.springframework.web.multipart.MultipartFile +import java.math.BigDecimal +import java.time.LocalDate + +data class SupportingDocument ( + val id: Long, + val skey: String, + val filename: String, +) + +data class Project ( + val id: Long, +) +data class SaveClaimDetailRequest ( + val id: Long, + val invoiceDate: LocalDate, + val description: String, + val project: Project, + val amount: BigDecimal, + val newSupportingDocument: MultipartFile?, + val oldSupportingDocument: SupportingDocument? +) + +data class SaveClaimRequest ( + @field:NotBlank(message = "Expense type cannot be empty") + val expenseType: String, + + val addClaimDetails: List, + + val status: String, + val id: Long?, +) \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimResponse.kt b/src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimResponse.kt new file mode 100644 index 0000000..28d256c --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimResponse.kt @@ -0,0 +1,8 @@ +package com.ffii.tsms.modules.claim.web.models + +import com.ffii.tsms.modules.claim.entity.Claim + +data class SaveClaimResponse( + val claim: Claim, + val message: String, +) \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt b/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt index b0fdcaa..00ce55d 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt @@ -117,7 +117,6 @@ open class StaffsService( staffRepository.save(staff) salaryEffectiveService.saveSalaryEffective(staff.id!!, salary.id!!) - logger.info(staff.id) return staff } @Transactional(rollbackFor = [Exception::class]) diff --git a/src/main/java/com/ffii/tsms/modules/file/entity/File.kt b/src/main/java/com/ffii/tsms/modules/file/entity/File.kt new file mode 100644 index 0000000..882afc9 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/entity/File.kt @@ -0,0 +1,39 @@ +package com.ffii.tsms.modules.file.entity + +import com.ffii.core.entity.BaseEntity +import com.ffii.core.entity.IdEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import java.math.BigInteger + +@Entity +@Table(name = "file") +open class File : BaseEntity() { + + open val DEFAULT_UPLOAD_MAX_FILE_SIZE_MB: Int = 50 + + @NotNull + @Column(name = "skey", length = 32) + open var skey: String? = null + + @NotNull + @Column(name = "filename", length = 255) + open var filename: String? = null + + @NotNull + @Column(name = "extension", length = 10) + open var description: String? = null + + @NotNull + @Column(name = "mimetype", length = 255) + open var mimetype: String? = null + + @NotNull + @Column(name = "filesize") + open var filesize: Long? = null + + @Column(name = "remarks", length = 500) + open var remarks: String? = null +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/file/entity/FileBlob.kt b/src/main/java/com/ffii/tsms/modules/file/entity/FileBlob.kt new file mode 100644 index 0000000..625c466 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/entity/FileBlob.kt @@ -0,0 +1,24 @@ +package com.ffii.tsms.modules.file.entity + +import com.ffii.core.entity.BaseEntity +import com.ffii.core.entity.IdEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull + +@Entity +@Table(name = "file_blob") +open class FileBlob : BaseEntity() { + + @NotNull + @OneToOne + @JoinColumn(name = "fileId") + open var file: File? = null + + @NotNull + @Column(name = "bytes") + open var bytes: ByteArray? = null +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/file/entity/FileBlobRepository.kt b/src/main/java/com/ffii/tsms/modules/file/entity/FileBlobRepository.kt new file mode 100644 index 0000000..cc84def --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/entity/FileBlobRepository.kt @@ -0,0 +1,9 @@ +package com.ffii.tsms.modules.file.entity + +import com.ffii.core.support.AbstractRepository +import org.springframework.data.repository.query.Param +import java.util.Optional + +interface FileBlobRepository : AbstractRepository { + +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/file/entity/FileRef.kt b/src/main/java/com/ffii/tsms/modules/file/entity/FileRef.kt new file mode 100644 index 0000000..e57a3f7 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/entity/FileRef.kt @@ -0,0 +1,31 @@ +package com.ffii.tsms.modules.file.entity + +import com.ffii.core.entity.BaseEntity +import com.ffii.core.entity.IdEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull + +@Entity +@Table(name = "file_ref") +open class FileRef : BaseEntity() { + + @NotNull + @Column(name = "refType", length = 20) + open var refType: String? = null + + @NotNull + @Column(name = "refId") + open var refId: Long? = null + + @Column(name = "refCode") + open var refCode: String? = null + + @NotNull + @OneToOne + @JoinColumn(name = "fileId") + open var file: File? = null +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/file/entity/FileRefRepository.kt b/src/main/java/com/ffii/tsms/modules/file/entity/FileRefRepository.kt new file mode 100644 index 0000000..6753329 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/entity/FileRefRepository.kt @@ -0,0 +1,6 @@ +package com.ffii.tsms.modules.file.entity + +import com.ffii.core.support.AbstractRepository + +interface FileRefRepository : AbstractRepository { +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/file/entity/FileRepository.kt b/src/main/java/com/ffii/tsms/modules/file/entity/FileRepository.kt new file mode 100644 index 0000000..88f8c7e --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/entity/FileRepository.kt @@ -0,0 +1,6 @@ +package com.ffii.tsms.modules.file.entity + +import com.ffii.core.support.AbstractRepository + +interface FileRepository : AbstractRepository { +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/file/service/FileService.kt b/src/main/java/com/ffii/tsms/modules/file/service/FileService.kt new file mode 100644 index 0000000..a1150c8 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/service/FileService.kt @@ -0,0 +1,326 @@ +package com.ffii.tsms.modules.file.service + +import com.ffii.core.support.AbstractBaseEntityService +import com.ffii.core.support.JdbcDao +import com.ffii.core.utils.FileUtils +import com.ffii.core.utils.NumberUtils +import com.ffii.core.utils.Params +import com.ffii.tsms.modules.file.entity.* +import org.apache.commons.lang3.RandomStringUtils +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import java.io.ByteArrayInputStream +import java.io.IOException +import java.util.* +import java.util.Map +import javax.imageio.ImageIO + + +@Service +open class FileService( + private val jdbcDao: JdbcDao, + private val fileRepository: FileRepository, + private val fileRefRepository: FileRefRepository, + private val fileBlobRepository: FileBlobRepository, +) : AbstractBaseEntityService(jdbcDao, fileRepository) { + + @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false) + open fun saveFile(instance: File): File { + return fileRepository.save(instance) + } + + @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false) + open fun saveFileBlob(instance: FileBlob): FileBlob { + return fileBlobRepository.save(instance) + } + + @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false) + open fun saveFileRef(instance: FileRef): FileRef { + return fileRefRepository.save(instance) + } + + @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true) + open fun findFileByIdAndKey(id: Long, skey: String): Optional { + return jdbcDao.queryForEntity( + "SELECT * from file f where f.id = :id and f.skey = :skey ", + Map.of("id", id, "skey", skey), File::class.java + ) + } + + @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true) + open fun findFileRefByTypeAndId(refType: String, refId: Long): List { + return jdbcDao.queryForList( + "SELECT * from file_ref where refType = :refType and refId = :refId order by id ", + java.util.Map.of("refType", refType, "refId", refId), FileRef::class.java + ) + } + + @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true) + open fun findFileRefByFileId(fileId: Long): Optional { + return jdbcDao.queryForEntity( + "SELECT * from file_ref where fileId = :fileId", java.util.Map.of("fileId", fileId), + FileRef::class.java + ) + } + + @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true) + open fun findFileBlobByFileId(fileId: Long): Optional { + return jdbcDao.queryForEntity( + "SELECT * from file_blob fb where fb.fileId = :fileId ", + java.util.Map.of("fileId", fileId), FileBlob::class.java + ) + } + + @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true) + open fun isFileExists(id: Long?, skey: String?): Boolean { + val sql = ("SELECT" + + " COUNT(1)" + + " FROM file f" + + " WHERE f.deleted = 0" + + " AND f.id = :id" + + " AND f.skey = :skey") + + val count = jdbcDao.queryForInt(sql, Map.of("id", id, "skey", skey)) + + return (count > 0) + } + + @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true) + open fun searchFiles(args: MutableMap): MutableList>? { + val sql = StringBuilder( + "SELECT" + + " f.id," + + " f.filename," + + " f.filesize," + + " f.skey," + + " fr.refId," + + " fr.refType," + + " f.created," + + " u.name AS createdByName," + + " f.description" + + " FROM file f" + + " LEFT JOIN file_ref fr ON f.id = fr.fileId" + + " LEFT JOIN user u ON f.createdBy = u.id" + + " WHERE f.deleted = 0" + ) + + if (args.containsKey("filename")) sql.append(" AND f.filename = :filename") + + if (args.containsKey("refType")) sql.append(" AND fr.refType = :refType") + + if (args.containsKey("refId")) sql.append(" AND fr.refId = :refId") + + if (args.containsKey("startDate")) sql.append(" AND f.created >= :startDate") + + if (args.containsKey("endDate")) sql.append(" AND f.created < :endDate") + + sql.append(" ORDER BY f.created DESC") + + return jdbcDao.queryForList(sql.toString(), args) + } + + /** + * Delete `FileRef` by `fileId`, `refId`, + * `refType`, and `skey` + */ + @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false) + open fun deleteFile(fileId: Long, refId: Long, refType: String, skey: String) { + val args = + Map.of("fileId", fileId, "refId", refId, "refType", refType, "skey", skey) + + jdbcDao.executeUpdate( + ("DELETE FROM file_ref" + + " WHERE fileId = :fileId" + + " AND refId = :refId" + + " AND refType = :refType" + + " AND EXISTS (SELECT 1 FROM file WHERE id = file_ref.fileId AND skey = :skey)"), + args + ) + } + + @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false) + open fun deleteFile(fileId: Long?, skey: String?, filename: String?) { + val args = Map.of("fileId", fileId, "skey", skey, "filename", filename) + + jdbcDao.executeUpdate( + ("DELETE FROM file_ref " + + " WHERE fileId = :fileId " + + " AND EXISTS (SELECT 1 FROM file WHERE id = file_ref.fileId AND skey = :skey AND filename = :filename)"), + args + ) + } + + @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false) + open fun saveFile( + filename: String, + description: String, + refType: String, + refId: Long, + refCode: String, + bytes: ByteArray + ): Long { + val file: File = File() + + file.apply { + this.filename = filename + this.description = description + mimetype = FileUtils.guessMimetype(filename) + filesize = NumberUtils.toLong(bytes.size, 0L) + skey = RandomStringUtils.randomAlphanumeric(32) + } + + val fileBlob: FileBlob = FileBlob() + + fileBlob.apply { + this.bytes = bytes + } + + val fileRef: FileRef = FileRef() + + fileRef.apply { + this.refId = refId + this.refType = refType + this.refCode = refCode + } + + // save File + fileRepository.save(file) + + // save FileBlob + fileBlob.apply { + this.file = file + } + fileBlobRepository.save(fileBlob) + + // save FileRef + fileRef.apply { + this.file = file + } + fileRefRepository.save(fileRef) + + return file.id!! + } + + @Transactional(rollbackFor = [Exception::class]) + @Throws(IOException::class) + open fun uploadFile( + multipartFile: MultipartFile, refId: Long?, refType: String?, refCode: String?, + maxSize: Int + ): MutableMap { + val result: MutableMap = HashMap() + + // filesize < 50MB + if (multipartFile.size > 0 && multipartFile.size <= maxSize * 1024 * 1024) { + // DEBUG LOG + + logger.info("multipartFile.getSize() = " + multipartFile.size) + + val file: File = File() + + file.apply { + filename = multipartFile.originalFilename + mimetype = FileUtils.guessMimetype(multipartFile.originalFilename) + filesize = multipartFile.size + skey = RandomStringUtils.randomAlphanumeric(32) + } + + // get height and width if mimetype is png or jpeg +// if (AbstractView.CONTENT_TYPE_PNG.equals(file.getMimetype()) +// || AbstractView.CONTENT_TYPE_JPEG.equals(file.getMimetype()) +// ) { +// val image = ImageIO.read(ByteArrayInputStream(fileBlob.getBytes())) +// if (image != null) { +// file.setImageHeight(image.height) +// file.setImageWidth(image.width) +// } +// } + + // create UserFile + saveFile(file) + + // create UserFileBlob + val fileBlob: FileBlob = FileBlob() + fileBlob.apply { + bytes = multipartFile.bytes + this.file = file + } + saveFileBlob(fileBlob) + + // create UserFileRef + val fileRef: FileRef = FileRef() + + fileRef.apply { + this.refId = refId + this.refType = refType + this.refCode = refCode + this.file = file + } + saveFileRef(fileRef) + + result["fileId"] = file.id!! + result["skey"] = file.skey!! + result["filename"] = file.filename!! + } else { + result["success"] = java.lang.Boolean.FALSE + result[Params.MSG] = "Upload Failed" + } + return result + } + + @Transactional(rollbackFor = [Exception::class]) + @Throws(IOException::class) + open fun updateFile( + multipartFile: MultipartFile, refId: Long?, refType: String?, refCode: String?, + maxSize: Int, fileId: Long + ): MutableMap { + val result: MutableMap = HashMap() + + // filesize < 50MB + if (multipartFile.size > 0 && multipartFile.size <= maxSize * 1024 * 1024) { + // DEBUG LOG + + logger.info("multipartFile.getSize() = " + multipartFile.size) + + val file: File = fileRepository.findById(fileId).orElse(File()) + + file.apply { + filename = multipartFile.originalFilename + mimetype = FileUtils.guessMimetype(multipartFile.originalFilename) + filesize = multipartFile.size + skey = RandomStringUtils.randomAlphanumeric(32) + } + + // create UserFile + saveFile(file) + + // create UserFileBlob + val fileBlob: FileBlob = findFileBlobByFileId(fileId).orElse(FileBlob()) + fileBlob.apply { + bytes = multipartFile.bytes + this.file = file + } + saveFileBlob(fileBlob) + + // create UserFileRef + val fileRef: FileRef = findFileRefByFileId(fileId).orElse(FileRef()) + + fileRef.apply { + this.refId = refId + this.refType = refType + this.refCode = refCode + this.file = file + } + saveFileRef(fileRef) + + result["fileId"] = file.id!! + result["skey"] = file.skey!! + result["filename"] = file.filename!! + } else { + result["success"] = java.lang.Boolean.FALSE + result[Params.MSG] = "Update Failed" + } + return result + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/file/web/DownloadFileContoller.kt b/src/main/java/com/ffii/tsms/modules/file/web/DownloadFileContoller.kt new file mode 100644 index 0000000..64dcb51 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/web/DownloadFileContoller.kt @@ -0,0 +1,133 @@ +package com.ffii.tsms.modules.file.web + +import com.ffii.core.utils.DateUtils +import com.ffii.core.utils.NumberUtils +import com.ffii.core.utils.StringUtils +import com.ffii.tsms.modules.file.entity.File +import com.ffii.tsms.modules.file.entity.FileBlob +import com.ffii.tsms.modules.file.service.FileService +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.beans.factory.annotation.Autowired +import java.text.DecimalFormat +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.io.IOException +import java.net.URLEncoder +import java.util.Date + +@RestController +@RequestMapping("/file") +open class DownloadFileController (private val fileService: FileService) { + private val logger: Log = LogFactory.getLog(javaClass) + + private val ZIP_NUMBERER : Boolean = false + + @GetMapping("/dl/{id}/{skey}/{filename}") + @Throws(IOException::class) + open fun download( + request: HttpServletRequest, response: HttpServletResponse, @RequestParam(defaultValue = "false") dl: Boolean, + @PathVariable id: Long, @PathVariable skey: String, @PathVariable filename: String + ) { + val file: File = fileService.findFileByIdAndKey(id, skey).get() + + if (file != null) { + val fileBlob: FileBlob = fileService.findFileBlobByFileId(id).get() + + response.reset() + response.contentType = file.mimetype + response.setContentLength(NumberUtils.toInt(file.filesize, 0)) + response.setHeader("Content-Transfer-Encoding", "binary") + response.setHeader("Access-Control-Expose-Headers", "Content-Disposition") + response.setHeader( + "Content-Disposition", + java.lang.String.format( + "%s; filename=\"%s\"", + if (dl) "attachment" else "inline", + URLEncoder.encode(file.filename, "UTF-8") + ) + ) + + + // String origin = request.getHeader("Origin"); + //TODO: anna Access-Control-Allow-Origin + response.addHeader("Access-Control-Allow-Origin", "*") + response.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET") + + val out = response.outputStream + + try { + out.write(fileBlob.bytes!!) + out.flush() + out.close() + } catch (e: IOException) { + logger.warn(e.message) + } finally { + out.close() + } + } else { + logger.info("*** 400 BAD REQUEST ***") + response.status = HttpServletResponse.SC_BAD_REQUEST + } + } + + @GetMapping(value = ["/dlz"], produces = ["application/zip"]) + @Throws(IOException::class) + open fun downloadAsZip( + response: HttpServletResponse, + @RequestParam filename: String, @RequestParam ids: String, @RequestParam skeys: String + ) { + var filename = filename + filename += DateUtils.formatDate(Date(), "_yyyy_MM_dd_HHmm", "") + ".zip" + response.setHeader("Access-Control-Expose-Headers", "Content-Disposition") + response.setHeader("Content-Transfer-Encoding", "binary") + response.setHeader( + "Content-Disposition", + String.format("%s; filename=\"%s\"", "attachment", response.encodeURL(filename)) + ) + + val df00 = DecimalFormat("00") + + val zipOutputStream = ZipOutputStream(response.outputStream) + + val idsA: Array = StringUtils.split(ids, ',') + val skeysA: Array = StringUtils.split(skeys, ',') + + var filePrefixIdx = 1 + + for (i in idsA.indices) { + val id: Long = NumberUtils.longValue(idsA[i]) + val skey = skeysA[i] + + val file: File = fileService.findFileByIdAndKey(id, skey).get() + + if (file == null) { + logger.warn("*** file is null, id = $id, skey = $skey") + } else { + val fileBlob: FileBlob = fileService.findFileBlobByFileId(id).get() + + if (fileBlob == null) { + logger.warn("*** fileBlob is null, id = $id, skey = $skey") + } else { + zipOutputStream.putNextEntry( + ZipEntry( + if (ZIP_NUMBERER) (df00.format(filePrefixIdx++.toLong()) + "_" + file.filename) + else file.filename + ) + ) + zipOutputStream.write(fileBlob.bytes) + zipOutputStream.closeEntry() + } + } + } + zipOutputStream.flush() + zipOutputStream.close() + } +} diff --git a/src/main/java/com/ffii/tsms/modules/file/web/FileController.kt b/src/main/java/com/ffii/tsms/modules/file/web/FileController.kt new file mode 100644 index 0000000..12f4a19 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/web/FileController.kt @@ -0,0 +1,67 @@ +package com.ffii.tsms.modules.file.web + +import com.ffii.core.response.RecordsRes +import com.ffii.core.utils.Params +import com.ffii.tsms.modules.file.entity.File +import com.ffii.tsms.modules.file.service.FileService +import com.ffii.tsms.modules.file.web.model.FileReq +import com.ffii.tsms.modules.file.web.model.UpdateFileReq +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import jakarta.validation.Valid +import org.springframework.ui.Model +import org.springframework.web.bind.ServletRequestBindingException +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping(value = ["/file"]) +open class FileController (private val fileService: FileService) { + + @PostMapping("/list") + @Throws(ServletRequestBindingException::class) + fun listJson( + model: Model?, + request: HttpServletRequest?, + @RequestBody req: @Valid FileReq + ): RecordsRes> { + val args: MutableMap = HashMap() + args["refType"] = req.refType + args["refId"] = req.refId + + val records: List>? = fileService.searchFiles(args) + + return RecordsRes(records) + } + + @PostMapping("/update") + fun update( + model: Model?, + response: HttpServletResponse?, + @RequestBody req: @Valid UpdateFileReq + ): Map { + val file: File = fileService.findFileByIdAndKey(req.fileId, req.skey).get() + val result: MutableMap = HashMap() + if (file != null) { + file.apply { + filename = req.filename + description = req.description + } + fileService.saveFile(file) + + result[Params.SUCCESS] = true + } else { + result[Params.SUCCESS] = false + } + return result + } + + @GetMapping("/delete/{fileId}/{skey}/{filename}") + fun delete(@PathVariable fileId: Long, @PathVariable skey: String, @PathVariable filename: String) { + fileService.deleteFile(fileId, skey, filename) + } +} diff --git a/src/main/java/com/ffii/tsms/modules/file/web/UploadFileController.kt b/src/main/java/com/ffii/tsms/modules/file/web/UploadFileController.kt new file mode 100644 index 0000000..2725e78 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/web/UploadFileController.kt @@ -0,0 +1,45 @@ +package com.ffii.tsms.modules.file.web + +import com.ffii.core.utils.StringUtils +import com.ffii.tsms.modules.file.entity.File +import com.ffii.tsms.modules.file.service.FileService +import com.ffii.tsms.modules.settings.service.SettingsService +import jakarta.servlet.http.HttpServletRequest +import org.apache.commons.logging.Log +import org.apache.commons.logging.LogFactory +import org.springframework.ui.Model +import org.springframework.web.bind.ServletRequestBindingException +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile +import java.io.IOException + +@RestController +@RequestMapping(value = ["/file"]) +class UploadFileController(private val settingsService: SettingsService, private val fileService: FileService) { + private val logger: Log = LogFactory.getLog(javaClass) + + @PostMapping("/ul") + @Throws(ServletRequestBindingException::class, IOException::class) + fun uploadFile( + request: HttpServletRequest, + model: Model, + @RequestParam refId: Long, + @RequestParam refType: String, + @RequestParam(defaultValue = StringUtils.EMPTY) refCode: String, + @RequestParam multipartFileList: List + ): Map { + var result: Map = HashMap() + + if (multipartFileList != null) { + for (multipartFile in multipartFileList) { + result = fileService.uploadFile(multipartFile, refId, refType, refCode, File().DEFAULT_UPLOAD_MAX_FILE_SIZE_MB) + } + } + + // only proceed if multipartFile is not null, and has file size + return result + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/file/web/model/FileReq.kt b/src/main/java/com/ffii/tsms/modules/file/web/model/FileReq.kt new file mode 100644 index 0000000..d5dadd1 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/web/model/FileReq.kt @@ -0,0 +1,10 @@ +package com.ffii.tsms.modules.file.web.model + +data class FileReq( + val fileId: Long, + val fileName: String, + val refType: String, + val refId: String, + val refCode: String, + val skey: String, +) \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/file/web/model/UpdateFileReq.kt b/src/main/java/com/ffii/tsms/modules/file/web/model/UpdateFileReq.kt new file mode 100644 index 0000000..d31b81b --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/file/web/model/UpdateFileReq.kt @@ -0,0 +1,8 @@ +package com.ffii.tsms.modules.file.web.model + +data class UpdateFileReq ( + val fileId: Long, + val filename: String, + val skey: String, + val description: String, +) \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/settings/service/SettingsService.java b/src/main/java/com/ffii/tsms/modules/settings/service/SettingsService.java index c1a8d19..c108690 100644 --- a/src/main/java/com/ffii/tsms/modules/settings/service/SettingsService.java +++ b/src/main/java/com/ffii/tsms/modules/settings/service/SettingsService.java @@ -134,6 +134,17 @@ public class SettingsService extends AbstractIdEntityService { + try { + return Integer.parseInt(v); + } catch (final NumberFormatException nfe) { + return defaultValue; + } + }).orElse(defaultValue); + } public double getDouble(String name) { return this.findByName(name) .map(Settings::getValue) diff --git a/src/main/resources/db/changelog/changes/20240419_01_cyril/01_update_claim.sql b/src/main/resources/db/changelog/changes/20240419_01_cyril/01_update_claim.sql new file mode 100644 index 0000000..b163d64 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240419_01_cyril/01_update_claim.sql @@ -0,0 +1,13 @@ +-- liquibase formatted sql +-- changeset cyril:update claim + +ALTER TABLE `claim` + CHANGE COLUMN `decision` `type` VARCHAR(20) NOT NULL , + ADD INDEX `FK_CLAIM_ON_VERIFIEDBY` (`verifiedBy` ASC) VISIBLE; +; +ALTER TABLE `claim` + ADD CONSTRAINT `FK_CLAIM_ON_VERIFIEDBY` + FOREIGN KEY (`verifiedBy`) + REFERENCES `staff` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION; diff --git a/src/main/resources/db/changelog/changes/20240419_01_cyril/02_update_claim.sql b/src/main/resources/db/changelog/changes/20240419_01_cyril/02_update_claim.sql new file mode 100644 index 0000000..a743d81 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240419_01_cyril/02_update_claim.sql @@ -0,0 +1,50 @@ +-- liquibase formatted sql +-- changeset cyril:create claim_detail, update claim + +CREATE TABLE `claim_detail` ( + `id` INT NOT NULL AUTO_INCREMENT, + `version` INT NOT NULL DEFAULT '0', + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `createdBy` VARCHAR(30) NULL DEFAULT NULL, + `modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `modifiedBy` VARCHAR(30) NULL DEFAULT NULL, + `deleted` TINYINT(1) NOT NULL DEFAULT '0', + `claimId` INT NOT NULL, + `invoiceDate` DATETIME NOT NULL, + `projectId` INT NOT NULL, + `description` VARCHAR(255) NOT NULL, + `amount` DECIMAL(14,2) NOT NULL, + `fileId` INT NOT NULL, + PRIMARY KEY (`id`), + INDEX `FK_CLAIM_DETAIL_ON_CLAIMID` (`claimId` ASC) VISIBLE, + INDEX `FK_CLAIM_DETAIL_ON_PROJECTID_` (`projectId` ASC) VISIBLE, + INDEX `FK_CLAIM_DETAIL_ON_FILEID` (`fileId` ASC) VISIBLE, + CONSTRAINT `FK_CLAIM_DETAIL_ON_CLAIMID` + FOREIGN KEY (`claimId`) + REFERENCES `claim` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT `FK_CLAIM_DETAIL_ON_PROJECTID` + FOREIGN KEY (`projectId`) + REFERENCES `project` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT `FK_CLAIM_DETAIL_ON_FILEID` + FOREIGN KEY (`fileId`) + REFERENCES `file` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION); + +ALTER TABLE `claim` +DROP FOREIGN KEY `FK_CLAIM_ON_PROJECTID`, +DROP FOREIGN KEY `FK_CLAIM_ON_FILEID`; +ALTER TABLE `claim` +DROP COLUMN `approvedAmount`, +DROP COLUMN `fileId`, +DROP COLUMN `amount`, +DROP COLUMN `description`, +DROP COLUMN `projectId`, +ADD COLUMN `status` VARCHAR(30) NOT NULL AFTER `type`, +DROP INDEX `FK_CLAIM_ON_FILEID` , +DROP INDEX `FK_CLAIM_ON_PROJECTID` ; +; \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20240419_01_cyril/03_update_claim.sql b/src/main/resources/db/changelog/changes/20240419_01_cyril/03_update_claim.sql new file mode 100644 index 0000000..e6c45af --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240419_01_cyril/03_update_claim.sql @@ -0,0 +1,6 @@ +-- liquibase formatted sql +-- changeset cyril:update claim + +ALTER TABLE `claim` +; +ALTER TABLE `claim` ALTER INDEX `FK_CLAIM_ON_STAFFID` VISIBLE; \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20240419_01_cyril/04_update_claim_detail.sql b/src/main/resources/db/changelog/changes/20240419_01_cyril/04_update_claim_detail.sql new file mode 100644 index 0000000..7c3e08a --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240419_01_cyril/04_update_claim_detail.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset cyril:update claim + +ALTER TABLE `claim_detail` + CHANGE COLUMN `invoiceDate` `invoiceDate` DATE NOT NULL ; diff --git a/src/main/resources/db/changelog/changes/20240424_01_cyril/01_update_claim.sql b/src/main/resources/db/changelog/changes/20240424_01_cyril/01_update_claim.sql new file mode 100644 index 0000000..b4bb056 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240424_01_cyril/01_update_claim.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset cyril:update claim + +ALTER TABLE `claim` + ADD COLUMN `code` VARCHAR(30) NULL AFTER `deleted`;