| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.fineract.portfolio.calendar.service; |
| |
| import java.text.DateFormat; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.StringTokenizer; |
| |
| import net.fortuna.ical4j.model.Date; |
| import net.fortuna.ical4j.model.DateList; |
| import net.fortuna.ical4j.model.DateTime; |
| import net.fortuna.ical4j.model.Recur; |
| import net.fortuna.ical4j.model.ValidationException; |
| import net.fortuna.ical4j.model.WeekDay; |
| import net.fortuna.ical4j.model.WeekDayList; |
| import net.fortuna.ical4j.model.parameter.Value; |
| import net.fortuna.ical4j.model.property.RRule; |
| |
| import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; |
| import org.apache.fineract.infrastructure.core.service.DateUtils; |
| import org.apache.fineract.organisation.workingdays.domain.WorkingDays; |
| import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil; |
| import org.apache.fineract.portfolio.calendar.domain.Calendar; |
| import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType; |
| import org.apache.fineract.portfolio.calendar.domain.CalendarWeekDaysType; |
| import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; |
| import org.joda.time.LocalDate; |
| import org.joda.time.format.DateTimeFormat; |
| import org.joda.time.format.DateTimeFormatter; |
| |
| public class CalendarUtils { |
| |
| static { |
| System.setProperty("net.fortuna.ical4j.timezone.date.floating", "true"); |
| } |
| |
| public static LocalDate getNextRecurringDate(final String recurringRule, final LocalDate seedDate, final LocalDate startDate) { |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| if (recur == null) { return null; } |
| LocalDate nextDate = getNextRecurringDate(recur, seedDate, startDate); |
| nextDate = adjustDate(nextDate, seedDate, getMeetingPeriodFrequencyType(recurringRule)); |
| return nextDate; |
| } |
| |
| public static LocalDate adjustDate(final LocalDate date, final LocalDate seedDate, final PeriodFrequencyType frequencyType) { |
| LocalDate adjustedVal = date; |
| if (frequencyType.isMonthly() && seedDate.getDayOfMonth() > 28) { |
| switch (date.getMonthOfYear()) { |
| case 2: |
| if (date.year().isLeap()) { |
| adjustedVal = date.dayOfMonth().setCopy(29); |
| } |
| break; |
| case 4: |
| case 6: |
| case 9: |
| case 11: |
| if (seedDate.getDayOfMonth() > 30) { |
| adjustedVal = date.dayOfMonth().setCopy(30); |
| } else { |
| adjustedVal = date.dayOfMonth().setCopy(seedDate.getDayOfMonth()); |
| } |
| break; |
| case 1: |
| case 3: |
| case 5: |
| case 7: |
| case 8: |
| case 10: |
| case 12: |
| adjustedVal = date.dayOfMonth().setCopy(seedDate.getDayOfMonth()); |
| break; |
| } |
| } |
| return adjustedVal; |
| } |
| |
| private static LocalDate getNextRecurringDate(final Recur recur, final LocalDate seedDate, final LocalDate startDate) { |
| final DateTime periodStart = new DateTime(startDate.toDate()); |
| final Date seed = convertToiCal4JCompatibleDate(seedDate); |
| final Date nextRecDate = recur.getNextDate(seed, periodStart); |
| return nextRecDate == null ? null : new LocalDate(nextRecDate); |
| } |
| |
| private static Date convertToiCal4JCompatibleDate(final LocalDate inputDate) { |
| // Date format in iCal4J is hard coded |
| Date formattedDate = null; |
| final DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); |
| final String seedDateStr = df.format(inputDate.toDateTimeAtStartOfDay().toDate()); |
| try { |
| formattedDate = new Date(seedDateStr, "yyyy-MM-dd"); |
| } catch (final ParseException e) { |
| e.printStackTrace(); |
| } |
| return formattedDate; |
| } |
| |
| public static Collection<LocalDate> getRecurringDates(final String recurringRule, final LocalDate seedDate, final LocalDate endDate) { |
| |
| final LocalDate periodStartDate = DateUtils.getLocalDateOfTenant(); |
| final LocalDate periodEndDate = (endDate == null) ? DateUtils.getLocalDateOfTenant().plusYears(5) : endDate; |
| return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate); |
| } |
| |
| public static Collection<LocalDate> getRecurringDatesFrom(final String recurringRule, final LocalDate seedDate, |
| final LocalDate startDate) { |
| final LocalDate periodStartDate = (startDate == null) ? DateUtils.getLocalDateOfTenant() : startDate; |
| final LocalDate periodEndDate = DateUtils.getLocalDateOfTenant().plusYears(5); |
| return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate); |
| } |
| |
| public static Collection<LocalDate> getRecurringDates(final String recurringRule, final LocalDate seedDate, |
| final LocalDate periodStartDate, final LocalDate periodEndDate) { |
| final int maxCount = 10;// Default number of recurring dates |
| return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate, maxCount); |
| } |
| |
| public static Collection<LocalDate> getRecurringDates(final String recurringRule, final LocalDate seedDate, |
| final LocalDate periodStartDate, final LocalDate periodEndDate, final int maxCount) { |
| |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| |
| return getRecurringDates(recur, seedDate, periodStartDate, periodEndDate, maxCount); |
| } |
| |
| private static Collection<LocalDate> getRecurringDates(final Recur recur, final LocalDate seedDate, final LocalDate periodStartDate, |
| final LocalDate periodEndDate, final int maxCount) { |
| if (recur == null) { return null; } |
| final Date seed = convertToiCal4JCompatibleDate(seedDate); |
| final DateTime periodStart = new DateTime(periodStartDate.toDate()); |
| final DateTime periodEnd = new DateTime(periodEndDate.toDate()); |
| |
| final Value value = new Value(Value.DATE.getValue()); |
| final DateList recurringDates = recur.getDates(seed, periodStart, periodEnd, value, maxCount); |
| return convertToLocalDateList(recurringDates, seedDate, getMeetingPeriodFrequencyType(recur)); |
| } |
| |
| private static Collection<LocalDate> convertToLocalDateList(final DateList dates, final LocalDate seedDate, |
| final PeriodFrequencyType frequencyType) { |
| |
| final Collection<LocalDate> recurringDates = new ArrayList<>(); |
| |
| for (@SuppressWarnings("rawtypes") |
| final Iterator iterator = dates.iterator(); iterator.hasNext();) { |
| final Date date = (Date) iterator.next(); |
| recurringDates.add(adjustDate(new LocalDate(date), seedDate, frequencyType)); |
| } |
| |
| return recurringDates; |
| } |
| |
| public static Recur getICalRecur(final String recurringRule) { |
| |
| // Construct RRule |
| try { |
| final RRule rrule = new RRule(recurringRule); |
| rrule.validate(); |
| |
| final Recur recur = rrule.getRecur(); |
| |
| return recur; |
| } catch (final ParseException e) { |
| // TODO Auto-generated catch block |
| e.printStackTrace(); |
| } catch (final ValidationException e) { |
| // TODO Auto-generated catch block |
| e.printStackTrace(); |
| } |
| |
| return null; |
| } |
| |
| public static String getRRuleReadable(final LocalDate startDate, final String recurringRule) { |
| |
| String humanReadable = ""; |
| |
| RRule rrule; |
| Recur recur = null; |
| try { |
| rrule = new RRule(recurringRule); |
| rrule.validate(); |
| recur = rrule.getRecur(); |
| } catch (final ValidationException e) { |
| throw new PlatformDataIntegrityException("error.msg.invalid.recurring.rule", "The Recurring Rule value: " + recurringRule |
| + " is not valid.", "recurrence", recurringRule); |
| } catch (final ParseException e) { |
| throw new PlatformDataIntegrityException("error.msg.recurring.rule.parsing.error", |
| "Error in pasring the Recurring Rule value: " + recurringRule, "recurrence", recurringRule); |
| } |
| |
| if (recur == null) { return humanReadable; } |
| |
| if (recur.getFrequency().equals(Recur.DAILY)) { |
| if (recur.getInterval() == 1) { |
| humanReadable = "Daily"; |
| } else { |
| humanReadable = "Every " + recur.getInterval() + " days"; |
| } |
| } else if (recur.getFrequency().equals(Recur.WEEKLY)) { |
| if (recur.getInterval() == 1 || recur.getInterval() == -1) { |
| humanReadable = "Weekly"; |
| } else { |
| humanReadable = "Every " + recur.getInterval() + " weeks"; |
| } |
| |
| humanReadable += " on "; |
| final WeekDayList weekDayList = recur.getDayList(); |
| |
| for (@SuppressWarnings("rawtypes") |
| final Iterator iterator = weekDayList.iterator(); iterator.hasNext();) { |
| final WeekDay weekDay = (WeekDay) iterator.next(); |
| humanReadable += DayNameEnum.from(weekDay.getDay()).getCode(); |
| } |
| |
| } else if (recur.getFrequency().equals(Recur.MONTHLY)) { |
| if (recur.getInterval() == 1) { |
| humanReadable = "Monthly on day " + startDate.getDayOfMonth(); |
| } else { |
| humanReadable = "Every " + recur.getInterval() + " months on day " + startDate.getDayOfMonth(); |
| } |
| } else if (recur.getFrequency().equals(Recur.YEARLY)) { |
| if (recur.getInterval() == 1) { |
| humanReadable = "Annually on " + startDate.toString("MMM") + " " + startDate.getDayOfMonth(); |
| } else { |
| humanReadable = "Every " + recur.getInterval() + " years on " + startDate.toString("MMM") + " " + startDate.getDayOfMonth(); |
| } |
| } |
| |
| if (recur.getCount() > 0) { |
| if (recur.getCount() == 1) { |
| humanReadable = "Once"; |
| } |
| humanReadable += ", " + recur.getCount() + " times"; |
| } |
| |
| final Date endDate = recur.getUntil(); |
| final LocalDate date = new LocalDate(endDate); |
| final DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MMMM YY"); |
| final String formattedDate = date.toString(fmt); |
| if (endDate != null) { |
| humanReadable += ", until " + formattedDate; |
| } |
| |
| return humanReadable; |
| } |
| |
| public static boolean isValidRedurringDate(final String recurringRule, final LocalDate seedDate, final LocalDate date) { |
| |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| if (recur == null) { return false; } |
| |
| return isValidRecurringDate(recur, seedDate, date); |
| } |
| |
| public static boolean isValidRecurringDate(final Recur recur, final LocalDate seedDate, final LocalDate date) { |
| |
| final Collection<LocalDate> recurDate = getRecurringDates(recur, seedDate, date, date.plusDays(1), 1); |
| return (recurDate == null || recurDate.isEmpty()) ? false : true; |
| } |
| |
| public static enum DayNameEnum { |
| MO(1, "Monday"), TU(2, "Tuesday"), WE(3, "Wednesday"), TH(4, "Thursday"), FR(5, "Friday"), SA(6, "Saturday"), SU(7, "Sunday"); |
| |
| private final String code; |
| private final Integer value; |
| |
| private DayNameEnum(final Integer value, final String code) { |
| this.value = value; |
| this.code = code; |
| } |
| |
| public String getCode() { |
| return this.code; |
| } |
| |
| public int getValue() { |
| return this.value; |
| } |
| |
| public static DayNameEnum from(final String name) { |
| for (final DayNameEnum dayName : DayNameEnum.values()) { |
| if (dayName.toString().equals(name)) { return dayName; } |
| } |
| return DayNameEnum.MO;// Default it to Monday |
| } |
| } |
| |
| public static PeriodFrequencyType getMeetingPeriodFrequencyType(final String recurringRule) { |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| return getMeetingPeriodFrequencyType(recur); |
| } |
| |
| private static PeriodFrequencyType getMeetingPeriodFrequencyType(final Recur recur) { |
| PeriodFrequencyType meetingFrequencyType = PeriodFrequencyType.INVALID; |
| if (recur.getFrequency().equals(Recur.DAILY)) { |
| meetingFrequencyType = PeriodFrequencyType.DAYS; |
| } else if (recur.getFrequency().equals(Recur.WEEKLY)) { |
| meetingFrequencyType = PeriodFrequencyType.WEEKS; |
| } else if (recur.getFrequency().equals(Recur.MONTHLY)) { |
| meetingFrequencyType = PeriodFrequencyType.MONTHS; |
| } else if (recur.getFrequency().equals(Recur.YEARLY)) { |
| meetingFrequencyType = PeriodFrequencyType.YEARS; |
| } |
| return meetingFrequencyType; |
| } |
| |
| public static String getMeetingFrequencyFromPeriodFrequencyType(final PeriodFrequencyType periodFrequency) { |
| String frequency = null; |
| if (periodFrequency.equals(PeriodFrequencyType.DAYS)) { |
| frequency = Recur.DAILY; |
| } else if (periodFrequency.equals(PeriodFrequencyType.WEEKS)) { |
| frequency = Recur.WEEKLY; |
| } else if (periodFrequency.equals(PeriodFrequencyType.MONTHS)) { |
| frequency = Recur.MONTHLY; |
| } else if (periodFrequency.equals(PeriodFrequencyType.YEARS)) { |
| frequency = Recur.YEARLY; |
| } |
| return frequency; |
| } |
| |
| public static int getInterval(final String recurringRule) { |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| return recur.getInterval(); |
| } |
| |
| public static CalendarFrequencyType getFrequency(final String recurringRule) { |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| return CalendarFrequencyType.fromString(recur.getFrequency()); |
| } |
| |
| public static CalendarWeekDaysType getRepeatsOnDay(final String recurringRule) { |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| final WeekDayList weekDays = recur.getDayList(); |
| if (weekDays.isEmpty()) return CalendarWeekDaysType.INVALID; |
| // supports only one day |
| WeekDay weekDay = (WeekDay) weekDays.get(0); |
| return CalendarWeekDaysType.fromString(weekDay.getDay()); |
| } |
| |
| public static LocalDate getFirstRepaymentMeetingDate(final Calendar calendar, final LocalDate disbursementDate, |
| final Integer loanRepaymentInterval, final String frequency) { |
| final Recur recur = CalendarUtils.getICalRecur(calendar.getRecurrence()); |
| if (recur == null) { return null; } |
| LocalDate startDate = disbursementDate; |
| final LocalDate seedDate = calendar.getStartDateLocalDate(); |
| if (isValidRedurringDate(calendar.getRecurrence(), seedDate, startDate)) { |
| startDate = startDate.plusDays(1); |
| } |
| // Recurring dates should follow loanRepaymentInterval. |
| // e.g. |
| // for weekly meeting interval is 1 |
| // where as for loan product with fortnightly frequency interval is 2 |
| // to generate currect set of meeting dates reset interval same as loan |
| // repayment interval. |
| recur.setInterval(loanRepaymentInterval); |
| |
| // Recurring dates should follow loanRepayment frequency. |
| // e.g. |
| // daily meeting frequency should support all loan products with any |
| // frequency type. |
| // to generate currect set of meeting dates reset frequency same as loan |
| // repayment frequency. |
| if (recur.getFrequency().equals(Recur.DAILY)) { |
| recur.setFrequency(frequency); |
| } |
| |
| final LocalDate firstRepaymentDate = getNextRecurringDate(recur, seedDate, startDate); |
| |
| return firstRepaymentDate; |
| } |
| |
| public static LocalDate getNewRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate, |
| final LocalDate oldRepaymentDate, final Integer loanRepaymentInterval, final String frequency, final WorkingDays workingDays) { |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| if (recur == null) { return null; } |
| if (isValidRecurringDate(recur, seedDate, oldRepaymentDate)) { return oldRepaymentDate; } |
| return getNextRepaymentMeetingDate(recurringRule, seedDate, oldRepaymentDate, loanRepaymentInterval, frequency, workingDays); |
| } |
| |
| public static LocalDate getNextRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate, |
| final LocalDate repaymentDate, final Integer loanRepaymentInterval, final String frequency, final WorkingDays workingDays) { |
| |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| if (recur == null) { return null; } |
| LocalDate tmpDate = repaymentDate; |
| if (isValidRecurringDate(recur, seedDate, repaymentDate)) { |
| tmpDate = repaymentDate.plusDays(1); |
| } |
| /* |
| * Recurring dates should follow loanRepaymentInterval. |
| * |
| * e.g. The weekly meeting will have interval of 1, if the loan product |
| * with fortnightly frequency will have interval of 2, to generate right |
| * set of meeting dates reset interval same as loan repayment interval. |
| */ |
| recur.setInterval(loanRepaymentInterval); |
| |
| /* |
| * Recurring dates should follow loanRepayment frequency. //e.g. daily |
| * meeting frequency should support all loan products with any type of |
| * frequency. to generate right set of meeting dates reset frequency |
| * same as loan repayment frequency. |
| */ |
| if (recur.getFrequency().equals(Recur.DAILY)) { |
| recur.setFrequency(frequency); |
| } |
| |
| LocalDate newRepaymentDate = getNextRecurringDate(recur, seedDate, tmpDate); |
| final LocalDate nextRepaymentDate = getNextRecurringDate(recur, seedDate, newRepaymentDate); |
| |
| newRepaymentDate = WorkingDaysUtil.getOffSetDateIfNonWorkingDay(newRepaymentDate, nextRepaymentDate, workingDays); |
| |
| return newRepaymentDate; |
| } |
| |
| public static boolean isFrequencySame(final String oldRRule, final String newRRule) { |
| final Recur oldRecur = getICalRecur(oldRRule); |
| final Recur newRecur = getICalRecur(newRRule); |
| |
| if (oldRecur == null || oldRecur.getFrequency() == null || newRecur == null || newRecur.getFrequency() == null) { return false; } |
| return oldRecur.getFrequency().equals(newRecur.getFrequency()); |
| } |
| |
| public static boolean isIntervalSame(final String oldRRule, final String newRRule) { |
| final Recur oldRecur = getICalRecur(oldRRule); |
| final Recur newRecur = getICalRecur(newRRule); |
| |
| if (oldRecur == null || oldRecur.getFrequency() == null || newRecur == null || newRecur.getFrequency() == null) { return false; } |
| return (oldRecur.getInterval() == newRecur.getInterval()); |
| } |
| |
| public static List<Integer> createIntegerListFromQueryParameter(final String calendarTypeQuery) { |
| final List<Integer> calendarTypeOptions = new ArrayList<>(); |
| // adding all calendar Types if query parameter is "all" |
| if (calendarTypeQuery.equalsIgnoreCase("all")) { |
| calendarTypeOptions.add(1); |
| calendarTypeOptions.add(2); |
| calendarTypeOptions.add(3); |
| calendarTypeOptions.add(4); |
| return calendarTypeOptions; |
| } |
| // creating a list of calendar type options from the comma separated |
| // query parameter. |
| final List<String> calendarTypeOptionsInQuery = new ArrayList<>(); |
| final StringTokenizer st = new StringTokenizer(calendarTypeQuery, ","); |
| while (st.hasMoreElements()) { |
| calendarTypeOptionsInQuery.add(st.nextElement().toString()); |
| } |
| |
| for (final String calType : calendarTypeOptionsInQuery) { |
| if (calType.equalsIgnoreCase("collection")) { |
| calendarTypeOptions.add(1); |
| } else if (calType.equalsIgnoreCase("training")) { |
| calendarTypeOptions.add(2); |
| } else if (calType.equalsIgnoreCase("audit")) { |
| calendarTypeOptions.add(3); |
| } else if (calType.equalsIgnoreCase("general")) { |
| calendarTypeOptions.add(4); |
| } |
| } |
| |
| return calendarTypeOptions; |
| } |
| |
| /** |
| * function returns a comma separated list of calendar_type_enum values ex. |
| * 1,2,3,4 |
| * |
| * @param calendarTypeOptions |
| * @return |
| */ |
| public static String getSqlCalendarTypeOptionsInString(final List<Integer> calendarTypeOptions) { |
| String sqlCalendarTypeOptions = ""; |
| final int size = calendarTypeOptions.size(); |
| for (int i = 0; i < size - 1; i++) { |
| sqlCalendarTypeOptions += calendarTypeOptions.get(i).toString() + ","; |
| } |
| sqlCalendarTypeOptions += calendarTypeOptions.get(size - 1).toString(); |
| return sqlCalendarTypeOptions; |
| } |
| |
| public static LocalDate getRecentEligibleMeetingDate(final String recurringRule, final LocalDate seedDate) { |
| LocalDate currentDate = DateUtils.getLocalDateOfTenant(); |
| final Recur recur = CalendarUtils.getICalRecur(recurringRule); |
| if (recur == null) { return null; } |
| |
| if (isValidRecurringDate(recur, seedDate, currentDate)) { return currentDate; } |
| |
| if (recur.getFrequency().equals(Recur.DAILY)) { |
| currentDate = currentDate.plusDays(recur.getInterval()); |
| } else if (recur.getFrequency().equals(Recur.WEEKLY)) { |
| currentDate = currentDate.plusWeeks(recur.getInterval()); |
| } else if (recur.getFrequency().equals(Recur.MONTHLY)) { |
| currentDate = currentDate.plusMonths(recur.getInterval()); |
| } else if (recur.getFrequency().equals(Recur.YEARLY)) { |
| currentDate = currentDate.plusYears(recur.getInterval()); |
| } |
| |
| return getNextRecurringDate(recur, seedDate, currentDate); |
| } |
| |
| public static LocalDate getNextScheduleDate(final Calendar calendar, final LocalDate startDate) { |
| final Recur recur = CalendarUtils.getICalRecur(calendar.getRecurrence()); |
| if (recur == null) { return null; } |
| LocalDate date = startDate; |
| final LocalDate seedDate = calendar.getStartDateLocalDate(); |
| /** |
| * if (isValidRedurringDate(calendar.getRecurrence(), seedDate, date)) { |
| * date = date.plusDays(1); } |
| **/ |
| |
| final LocalDate scheduleDate = getNextRecurringDate(recur, seedDate, date); |
| |
| return scheduleDate; |
| } |
| } |