/* * Copyright (c) 2005-2011 Grameen Foundation USA * All rights reserved. * * Licensed 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. * * See also http://www.apache.org/licenses/LICENSE-2.0.html for an * explanation of the license and how it is applied. */ package org.mifos.framework.components.batchjobs.helpers; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.joda.time.DateTime; import org.joda.time.Days; import org.mifos.accounts.business.AccountActionDateEntity; import org.mifos.accounts.persistence.LegacyAccountDao; import org.mifos.accounts.savings.persistence.GenericDaoHibernate; import org.mifos.application.holiday.business.Holiday; import org.mifos.application.holiday.persistence.HolidayDao; import org.mifos.application.holiday.persistence.HolidayDaoHibernate; import org.mifos.application.holiday.util.helpers.RepaymentRuleTypes; import org.mifos.application.meeting.business.MeetingBO; import org.mifos.application.servicefacade.ApplicationContextProvider; import org.mifos.config.FiscalCalendarRules; import org.mifos.framework.components.batchjobs.SchedulerConstants; import org.mifos.framework.components.batchjobs.TaskHelper; import org.mifos.framework.components.batchjobs.configuration.BatchJobConfigurationService; import org.mifos.framework.components.batchjobs.configuration.StandardBatchJobConfigurationService; import org.mifos.framework.components.batchjobs.exceptions.BatchJobException; import org.mifos.framework.exceptions.PersistenceException; import org.mifos.framework.hibernate.helper.StaticHibernateUtil; import org.mifos.framework.util.DateTimeService; import org.mifos.framework.util.helpers.DateUtils; import org.mifos.schedule.ScheduledDateGeneration; import org.mifos.schedule.ScheduledEvent; import org.mifos.schedule.ScheduledEventFactory; import org.mifos.schedule.internal.HolidayAndWorkingDaysAndMoratoriaScheduledDateGeneration; public class ApplyHolidayChangesHelper extends TaskHelper { // injectable external dependencies private LegacyAccountDao legacyAccountDao; private BatchJobConfigurationService batchJobConfigurationService; private HolidayDao holidayDao; private FiscalCalendarRules fiscalCalendarRules; private Map<Short, ScheduledDateGeneration> officeScheduledDateGenerationMap; private int outputIntervalForBatchJobs; private int batchSize; private int recordCommittingSize; private long rollingStartTime; private int accountCount; private int currentRecordNumber; private List<Days> workingDays; private List<String> errorList; private List<Holiday> unappliedHolidays; public ApplyHolidayChangesHelper() { super(); } public BatchJobConfigurationService getBatchJobConfigurationService() { if (batchJobConfigurationService == null) { return new StandardBatchJobConfigurationService(); } return this.batchJobConfigurationService; } public void setBatchJobConfigurationService(BatchJobConfigurationService batchJobConfigurationService) { this.batchJobConfigurationService = batchJobConfigurationService; } public LegacyAccountDao getlegacyAccountDao() { if (legacyAccountDao == null) { return ApplicationContextProvider.getBean(LegacyAccountDao.class); } return this.legacyAccountDao; } public void setlegacyAccountDao(LegacyAccountDao legacyAccountDao) { this.legacyAccountDao = legacyAccountDao; } public HolidayDao getHolidayDao() { if (holidayDao == null) { return new HolidayDaoHibernate(new GenericDaoHibernate()); } return this.holidayDao; } public void setHolidayDao(HolidayDao holidayDao) { this.holidayDao = holidayDao; } public FiscalCalendarRules getFiscalCalendarRules() { if (fiscalCalendarRules == null) { fiscalCalendarRules = new FiscalCalendarRules(); } return this.fiscalCalendarRules; } public void setFiscalCalendarRules(FiscalCalendarRules fiscalCalendarRules) { this.fiscalCalendarRules = fiscalCalendarRules; } @Override public void execute(long timeInMillis) throws BatchJobException { long taskStartTime = new DateTimeService().getCurrentDateTime().getMillis(); initializeTaskGlobalParameters(); if (unappliedHolidays != null && !unappliedHolidays.isEmpty()) { for (Holiday holiday : unappliedHolidays) { logMessage("Processing Holiday: " + holiday.getName() + " From: " + DateUtils.getLocalDateFromDate(holiday.getFromDate().toDate()) + " To: " + DateUtils.getLocalDateFromDate(holiday.getThruDate().toDate())); try { rescheduleDatesStartingFromUnappliedHoliday(holiday); } catch (Exception e) { StaticHibernateUtil.rollbackTransaction(); errorList.add("Failed to apply holiday changes: " + e.toString()); e.printStackTrace(); throw new BatchJobException(SchedulerConstants.FAILURE, errorList); } } } StaticHibernateUtil.closeSession(); String finalMessage = "ApplyHolidayChanges task completed in " + (new DateTimeService().getCurrentDateTime().getMillis() - taskStartTime) + " ms"; logMessage(finalMessage); } private void rescheduleDatesStartingFromUnappliedHoliday(Holiday holiday) throws PersistenceException { long holidayStartTime = new DateTimeService().getCurrentDateTime().getMillis(); reschedule(holiday, new SavingsAccountBatch()); reschedule(holiday, new CustomerAccountBatch()); reschedule(holiday, new LoanAccountBatch()); applyHoliday(holiday); String endHolidayMessage = "Completed Processing for Holiday: " + holiday.getName() + " Time Taken: " + (new DateTimeService().getCurrentDateTime().getMillis() - holidayStartTime) + " ms"; logMessage(endHolidayMessage); } private void reschedule(Holiday holiday, AccountBatch accountBatch) throws PersistenceException { long rescheduleStartTime = new DateTimeService().getCurrentDateTime().getMillis(); List<Object[]> accountIdsArray = accountBatch.getAccountIdsWithDatesIn(holiday); accountCount = accountIdsArray.size(); logMessage("No. of " + accountBatch.getAccountTypeName() + " Accounts to Process: " + accountCount + " : Query took: " + (new DateTimeService().getCurrentDateTime().getMillis() - rescheduleStartTime) + " ms"); rollingStartTime = new DateTimeService().getCurrentDateTime().getMillis(); currentRecordNumber = 0; StaticHibernateUtil.getSessionTL(); StaticHibernateUtil.startTransaction(); for (Object[] accountIds : accountIdsArray) { Integer accountId = (Integer) accountIds[0]; Short officeId = (Short) accountIds[1]; Integer meetingId = (Integer) accountIds[2]; currentRecordNumber++; ScheduledDateGeneration officeScheduledDateGeneration = getScheduledDateGeneration(officeId); DateTime amendedThruDate = holiday.getThruDate(); // Moratoria affect all installments on or after the fromDate. // Normal holidays only affect installments between the fromDate and thruDate if (holiday.getRepaymentRuleType().getValue().equals(RepaymentRuleTypes.REPAYMENT_MORATORIUM.getValue())) { amendedThruDate = holiday.getThruDate().plusYears(10); } List<AccountActionDateEntity> futureAffectedInstallments = accountBatch.getAffectedInstallments(accountId, holiday.getFromDate(), amendedThruDate); MeetingBO meeting = (MeetingBO) StaticHibernateUtil.getSessionTL().get(MeetingBO.class, meetingId); rescheduleDatesForNewHolidays(officeScheduledDateGeneration, futureAffectedInstallments, meeting); houseKeeping(); } StaticHibernateUtil.commitTransaction(); long rescheduleEndTime = new DateTimeService().getCurrentDateTime().getMillis(); String message = "" + currentRecordNumber + " updated, " + (accountCount - currentRecordNumber) + " remaining, batch time: " + (rescheduleEndTime - rollingStartTime) + " ms"; logMessage(message); String finalMessage = accountBatch.getAccountTypeName() + " accounts Processed in: " + (rescheduleEndTime - rescheduleStartTime) + " ms"; logMessage(finalMessage); } /** * Shift schedule dates to account for new holidays, starting with the first future or present installment presented */ private void rescheduleDatesForNewHolidays(final ScheduledDateGeneration scheduledDateGeneration, List<AccountActionDateEntity> futureAffectedInstallments, final MeetingBO meeting) { List<DateTime> installmentDates = getDatesToReplaceScheduledDates(scheduledDateGeneration, futureAffectedInstallments, meeting); int counter = 0; for (AccountActionDateEntity actionDate : futureAffectedInstallments) { actionDate.setActionDate(new java.sql.Date(installmentDates.get(counter).toDate().getTime())); counter++; } } private List<DateTime> getDatesToReplaceScheduledDates(final ScheduledDateGeneration dateGeneration, List<AccountActionDateEntity> installments, final MeetingBO meeting) { ScheduledEvent scheduledEvent = ScheduledEventFactory.createScheduledEventFrom(meeting); int numberOfDatesToGenerate = installments.size() + 1; DateTime dayBeforeFirstDateToGenerate = new DateTime(installments.get(0).getActionDate()).minusDays(1); return dateGeneration.generateScheduledDates(numberOfDatesToGenerate, dayBeforeFirstDateToGenerate, scheduledEvent, false); } private ScheduledDateGeneration getScheduledDateGeneration(Short officeId) { ScheduledDateGeneration scheduledDateGeneration = officeScheduledDateGenerationMap.get(officeId); if (scheduledDateGeneration != null) { return scheduledDateGeneration; } List<Holiday> futureHolidays = getHolidayDao().findCurrentAndFutureOfficeHolidaysEarliestFirst(officeId); scheduledDateGeneration = new HolidayAndWorkingDaysAndMoratoriaScheduledDateGeneration(workingDays, futureHolidays); officeScheduledDateGenerationMap.put(officeId, scheduledDateGeneration); return scheduledDateGeneration; } private void applyHoliday(Holiday holiday) throws PersistenceException { StaticHibernateUtil.getSessionTL(); StaticHibernateUtil.startTransaction(); Holiday updateHoliday = getHolidayDao().findHolidayById(holiday.getId()); updateHoliday.markAsApplied(); getHolidayDao().save(updateHoliday); StaticHibernateUtil.commitTransaction(); } private void initializeTaskGlobalParameters() { outputIntervalForBatchJobs = getBatchJobConfigurationService().getOutputIntervalForBatchJobs(); batchSize = getBatchJobConfigurationService().getBatchSizeForBatchJobs(); recordCommittingSize = 500; errorList = new ArrayList<String>(); workingDays = new FiscalCalendarRules().getWorkingDaysAsJodaTimeDays(); unappliedHolidays = getHolidayDao().getUnAppliedHolidays(); officeScheduledDateGenerationMap = new HashMap<Short, ScheduledDateGeneration>(); } private void houseKeeping() { if (currentRecordNumber % batchSize == 0) { StaticHibernateUtil.flushAndClearSession(); } if (currentRecordNumber % recordCommittingSize == 0) { StaticHibernateUtil.commitTransaction(); StaticHibernateUtil.getSessionTL(); StaticHibernateUtil.startTransaction(); } if (currentRecordNumber % outputIntervalForBatchJobs == 0) { long time = new DateTimeService().getCurrentDateTime().getMillis(); String message = "" + currentRecordNumber + " updated, " + (accountCount - currentRecordNumber) + " remaining, batch time: " + (time - rollingStartTime) + " ms"; logMessage(message); rollingStartTime = time; } } private void logMessage(String finalMessage) { getLogger().info(finalMessage); } private interface AccountBatch { List<Object[]> getAccountIdsWithDatesIn(Holiday holiday) throws PersistenceException; List<AccountActionDateEntity> getAffectedInstallments(Integer accountId, DateTime fromDate, DateTime thruDate) throws PersistenceException; String getAccountTypeName(); } private abstract class AbstractAccountBatch implements AccountBatch { @Override public List<Object[]> getAccountIdsWithDatesIn(Holiday holiday) throws PersistenceException { return getAccountIdsHavingSchedulesWithinHoliday(holiday); } @Override public List<AccountActionDateEntity> getAffectedInstallments(Integer accountId, DateTime fromDate, DateTime thruDate) throws PersistenceException { return getAffectedInstallmentsForAccountType(accountId, fromDate, thruDate); } @Override public String getAccountTypeName() { return "Abstract Account Type"; } protected abstract List<Object[]> getAccountIdsHavingSchedulesWithinHoliday(Holiday holiday) throws PersistenceException; protected abstract List<AccountActionDateEntity> getAffectedInstallmentsForAccountType(Integer accountId, DateTime fromDate, DateTime thruDate) throws PersistenceException; } private class LoanAccountBatch extends AbstractAccountBatch { @Override public List<Object[]> getAccountIdsHavingSchedulesWithinHoliday(Holiday holiday) throws PersistenceException { return getlegacyAccountDao().getListOfAccountIdsHavingLoanSchedulesWithinAHoliday(holiday); } @Override public List<AccountActionDateEntity> getAffectedInstallmentsForAccountType(Integer accountId, DateTime fromDate, DateTime thruDate) throws PersistenceException { List<AccountActionDateEntity> affectedInstallmentsGeneric = new ArrayList<AccountActionDateEntity>(); affectedInstallmentsGeneric.addAll(getlegacyAccountDao().getLoanSchedulesForAccountThatAreWithinDates( accountId, fromDate, thruDate)); return affectedInstallmentsGeneric; } @Override public String getAccountTypeName() { return "Loan"; } } private class SavingsAccountBatch extends AbstractAccountBatch { @Override public List<Object[]> getAccountIdsHavingSchedulesWithinHoliday(Holiday holiday) throws PersistenceException { return getlegacyAccountDao().getListOfAccountIdsHavingSavingsSchedulesWithinAHoliday(holiday); } @Override public List<AccountActionDateEntity> getAffectedInstallmentsForAccountType(Integer accountId, DateTime fromDate, DateTime thruDate) throws PersistenceException { List<AccountActionDateEntity> affectedInstallmentsGeneric = new ArrayList<AccountActionDateEntity>(); affectedInstallmentsGeneric.addAll(getlegacyAccountDao().getSavingsSchedulesForAccountThatAreWithinDates( accountId, fromDate, thruDate)); return affectedInstallmentsGeneric; } @Override public String getAccountTypeName() { return "Savings"; } } private class CustomerAccountBatch extends AbstractAccountBatch { @Override public List<Object[]> getAccountIdsHavingSchedulesWithinHoliday(Holiday holiday) throws PersistenceException { return getlegacyAccountDao().getListOfAccountIdsHavingCustomerSchedulesWithinAHoliday(holiday); } @Override public List<AccountActionDateEntity> getAffectedInstallmentsForAccountType(Integer accountId, DateTime fromDate, DateTime thruDate) throws PersistenceException { List<AccountActionDateEntity> affectedInstallmentsGeneric = new ArrayList<AccountActionDateEntity>(); affectedInstallmentsGeneric.addAll(getlegacyAccountDao() .getCustomerSchedulesForAccountThatAreWithinDates(accountId, fromDate, thruDate)); return affectedInstallmentsGeneric; } @Override public String getAccountTypeName() { return "Customer"; } } }