/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.module.ld.batch.service.impl; import java.io.File; import java.io.FileFilter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.commons.io.filefilter.SuffixFileFilter; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.gl.batch.service.EnterpriseFeederNotificationService; import org.kuali.kfs.gl.batch.service.impl.RequiredFilesMissingStatus; import org.kuali.kfs.gl.report.LedgerSummaryReport; import org.kuali.kfs.gl.service.impl.EnterpriseFeederStatusAndErrorMessagesWrapper; import org.kuali.kfs.module.ld.LaborConstants; import org.kuali.kfs.module.ld.LaborKeyConstants; import org.kuali.kfs.module.ld.batch.LaborEnterpriseFeedStep; import org.kuali.kfs.module.ld.batch.service.EnterpriseFeederService; import org.kuali.kfs.module.ld.batch.service.FileEnterpriseFeederHelperService; import org.kuali.kfs.module.ld.report.EnterpriseFeederReportData; import org.kuali.kfs.sys.Message; import org.kuali.kfs.sys.batch.InitiateDirectoryBase; import org.kuali.kfs.sys.service.ReportWriterService; import org.kuali.rice.core.api.config.property.ConfigurationService; import org.kuali.rice.core.api.datetime.DateTimeService; import org.kuali.rice.coreservice.framework.parameter.ParameterService; /** * This class iterates through the files in the enterprise feeder staging directory, which is injected by Spring. Note: this class * is NOT annotated as transactional. This allows the helper service, which is defined as transactional, to do a per-file * transaction. */ public class FileEnterpriseFeederServiceImpl extends InitiateDirectoryBase implements EnterpriseFeederService { private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(FileEnterpriseFeederServiceImpl.class); protected String directoryName; protected String laborOriginEntryDirectoryName; protected DateTimeService dateTimeService; protected FileEnterpriseFeederHelperService fileEnterpriseFeederHelperService; protected EnterpriseFeederNotificationService enterpriseFeederNotificationService; protected ParameterService parameterService; protected ConfigurationService configurationService; protected String reconciliationTableId; protected ReportWriterService reportWriterService; protected ReportWriterService errorStatisticsReport; /** * Feeds file sets in the directory whose name is returned by the invocation to getDirectoryName() * * @see org.kuali.kfs.gl.batch.service.EnterpriseFeederService#feed(java.lang.String) */ @Override public void feed(String processName, boolean performNotifications) { // ensure that this feeder implementation may not be run concurrently on this JVM // to consider: maybe use java NIO classes to perform done file locking? synchronized (this) { if (StringUtils.isBlank(directoryName)) { throw new IllegalArgumentException("directoryName not set for FileEnterpriseFeederServiceImpl."); } FileFilter doneFileFilter = new SuffixFileFilter(DONE_FILE_SUFFIX); File enterpriseFeedFile = null; String enterpriseFeedFileName = LaborConstants.BatchFileSystem.LABOR_ENTERPRISE_FEED + LaborConstants.BatchFileSystem.EXTENSION; enterpriseFeedFile = new File(laborOriginEntryDirectoryName + File.separator + enterpriseFeedFileName); PrintStream enterpriseFeedPs = null; try { enterpriseFeedPs = new PrintStream(enterpriseFeedFile); } catch (FileNotFoundException e) { LOG.error("enterpriseFeedFile doesn't exist " + enterpriseFeedFileName); throw new RuntimeException("enterpriseFeedFile doesn't exist " + enterpriseFeedFileName); } if ( LOG.isInfoEnabled() ) { LOG.info("New File created for enterprise feeder service run: " + enterpriseFeedFileName); } File directory = new File(directoryName); if (!directory.exists() || !directory.isDirectory()) { LOG.error("Directory doesn't exist and or it's not really a directory " + directoryName); throw new RuntimeException("Directory doesn't exist and or it's not really a directory " + directoryName); } File[] doneFiles = directory.listFiles(doneFileFilter); reorderDoneFiles(doneFiles); LedgerSummaryReport ledgerSummaryReport = new LedgerSummaryReport(); // keeps track of statistics for reporting EnterpriseFeederReportData feederReportData = new EnterpriseFeederReportData(); List<EnterpriseFeederStatusAndErrorMessagesWrapper> statusAndErrorsList = new ArrayList<EnterpriseFeederStatusAndErrorMessagesWrapper>(); for (File doneFile : doneFiles) { File dataFile = null; File reconFile = null; EnterpriseFeederStatusAndErrorMessagesWrapper statusAndErrors = new EnterpriseFeederStatusAndErrorMessagesWrapper(); statusAndErrors.setErrorMessages(new ArrayList<Message>()); dataFile = getDataFile(doneFile); reconFile = getReconFile(doneFile); statusAndErrors.setFileNames(dataFile, reconFile, doneFile); if (dataFile == null) { LOG.error("Unable to find data file for done file: " + doneFile.getAbsolutePath()); statusAndErrors.getErrorMessages().add( new Message("Unable to find data file for done file: " + doneFile.getAbsolutePath(), Message.TYPE_FATAL)); statusAndErrors.setStatus(new RequiredFilesMissingStatus()); } if (reconFile == null) { LOG.error("Unable to find recon file for done file: " + doneFile.getAbsolutePath()); statusAndErrors.getErrorMessages().add( new Message("Unable to find recon file for done file: " + doneFile.getAbsolutePath(), Message.TYPE_FATAL)); statusAndErrors.setStatus(new RequiredFilesMissingStatus()); } try { if (dataFile != null && reconFile != null) { if ( LOG.isInfoEnabled() ) { LOG.info("Data file: " + dataFile.getAbsolutePath()); LOG.info("Reconciliation File: " + reconFile.getAbsolutePath()); } fileEnterpriseFeederHelperService.feedOnFile(doneFile, dataFile, reconFile, enterpriseFeedPs, processName, reconciliationTableId, statusAndErrors, ledgerSummaryReport, errorStatisticsReport, feederReportData); } } catch (RuntimeException e) { // we need to be extremely resistant to a file load failing so that it doesn't prevent other files from loading LOG.error("Caught exception when feeding done file: " + doneFile.getAbsolutePath()); } finally { statusAndErrorsList.add(statusAndErrors); boolean doneFileDeleted = doneFile.delete(); if (!doneFileDeleted) { statusAndErrors.getErrorMessages().add( new Message("Unable to delete done file: " + doneFile.getAbsolutePath(), Message.TYPE_FATAL)); } if (performNotifications) { enterpriseFeederNotificationService.notifyFileFeedStatus(processName, statusAndErrors.getStatus(), doneFile, dataFile, reconFile, statusAndErrors.getErrorMessages()); } } } enterpriseFeedPs.close(); // if errors encountered is greater than max allowed the enterprise feed file should not be sent boolean enterpriseFeedFileCreated = false; if (feederReportData.getNumberOfErrorEncountered() > getMaximumNumberOfErrorsAllowed()) { enterpriseFeedFile.delete(); } else { // generate done file String enterpriseFeedDoneFileName = enterpriseFeedFileName.replace( LaborConstants.BatchFileSystem.EXTENSION, LaborConstants.BatchFileSystem.DONE_FILE_EXTENSION); File enterpriseFeedDoneFile = new File(laborOriginEntryDirectoryName + File.separator + enterpriseFeedDoneFileName); if (!enterpriseFeedDoneFile.exists()) { try { enterpriseFeedDoneFile.createNewFile(); } catch (IOException e) { LOG.error("Unable to create done file for enterprise feed output group.", e); throw new RuntimeException("Unable to create done file for enterprise feed output group.", e); } } enterpriseFeedFileCreated = true; } // write out totals to log file if ( LOG.isInfoEnabled() ) { LOG.info("Total records read: " + feederReportData.getNumberOfRecordsRead()); LOG.info("Total amount read: " + feederReportData.getTotalAmountRead()); LOG.info("Total records written: " + feederReportData.getNumberOfRecordsRead()); LOG.info("Total amount written: " + feederReportData.getTotalAmountWritten()); } generateReport(enterpriseFeedFileCreated, feederReportData, statusAndErrorsList, ledgerSummaryReport, laborOriginEntryDirectoryName + File.separator + enterpriseFeedFileName); } } /** * Sets the laborOriginEntryDirectoryName attribute value. * @param laborOriginEntryDirectoryName The laborOriginEntryDirectoryName to set. */ public void setLaborOriginEntryDirectoryName(String laborOriginEntryDirectoryName) { this.laborOriginEntryDirectoryName = laborOriginEntryDirectoryName; } /** * Reorders the files in case there's a dependency on the order in which files are fed upon. For this implementation, the * purpose is to always order files in a way such that unit testing will be predictable. * * @param doneFiles */ protected void reorderDoneFiles(File[] doneFiles) { // sort the list so that the unit tests will have more predictable results Arrays.sort(doneFiles); } /** * Given the doneFile, this method finds the data file corresponding to the done file * * @param doneFile * @return a File for the data file, or null if the file doesn't exist or is not readable */ protected File getDataFile(File doneFile) { String doneFileAbsPath = doneFile.getAbsolutePath(); if (!doneFileAbsPath.endsWith(DONE_FILE_SUFFIX)) { LOG.error("Done file name must end with " + DONE_FILE_SUFFIX); throw new IllegalArgumentException("Done file name must end with " + DONE_FILE_SUFFIX); } String dataFileAbsPath = StringUtils.removeEnd(doneFileAbsPath, DONE_FILE_SUFFIX) + DATA_FILE_SUFFIX; File dataFile = new File(dataFileAbsPath); if (!dataFile.exists() || !dataFile.canRead()) { LOG.error("Cannot find/read data file " + dataFileAbsPath); return null; } return dataFile; } /** * Given the doneFile, this method finds the reconciliation file corresponding to the data file * * @param doneFile * @return a file for the reconciliation data, or null if the file doesn't exist or is not readable */ protected File getReconFile(File doneFile) { String doneFileAbsPath = doneFile.getAbsolutePath(); if (!doneFileAbsPath.endsWith(DONE_FILE_SUFFIX)) { LOG.error("Done file name must end with " + DONE_FILE_SUFFIX); throw new IllegalArgumentException("DOne file name must end with " + DONE_FILE_SUFFIX); } String reconFileAbsPath = StringUtils.removeEnd(doneFileAbsPath, DONE_FILE_SUFFIX) + RECON_FILE_SUFFIX; File reconFile = new File(reconFileAbsPath); if (!reconFile.exists() || !reconFile.canRead()) { LOG.error("Cannot find/read data file " + reconFileAbsPath); return null; } return reconFile; } /** * Gets the directoryName attribute. * * @return Returns the directoryName. */ @Override public String getDirectoryName() { return directoryName; } /** * Sets the directoryName attribute value. * * @param directoryName The directoryName to set. */ public void setDirectoryName(String directoryName) { this.directoryName = directoryName; } public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } public void setFileEnterpriseFeederHelperService(FileEnterpriseFeederHelperService fileEnterpriseFeederHelperServiceImpl) { this.fileEnterpriseFeederHelperService = fileEnterpriseFeederHelperServiceImpl; } public void setEnterpriseFeederNotificationService(EnterpriseFeederNotificationService enterpriseFeederNotificationService) { this.enterpriseFeederNotificationService = enterpriseFeederNotificationService; } /** * Gets the reconciliationTableId attribute. * * @return Returns the reconciliationTableId. */ public String getReconciliationTableId() { return reconciliationTableId; } /** * Sets the reconciliationTableId attribute value. * * @param reconciliationTableId The reconciliationTableId to set. */ public void setReconciliationTableId(String reconciliationTableId) { this.reconciliationTableId = reconciliationTableId; } protected void generateReport(boolean enterpriseFeedFileCreated, EnterpriseFeederReportData feederReportData, List<EnterpriseFeederStatusAndErrorMessagesWrapper> statusAndErrorsList, LedgerSummaryReport report, String outputFileName) { if (enterpriseFeedFileCreated) { reportWriterService.writeFormattedMessageLine("Output File Name: %s", outputFileName); } else { reportWriterService .writeFormattedMessageLine(configurationService.getPropertyValueAsString(LaborKeyConstants.EnterpriseFeed.ERROR_OUTPUT_FILE_NOT_GENERATED)); } reportWriterService.writeNewLines(1); generateFilesLoadedStatusReport(statusAndErrorsList); reportWriterService.pageBreak(); report.writeReport(reportWriterService); generateErrorAndStatisticsReport(feederReportData); } protected void generateFilesLoadedStatusReport(List<EnterpriseFeederStatusAndErrorMessagesWrapper> statusAndErrorsList) { boolean successfulFileLoaded = false; reportWriterService.writeSubTitle("Files Successfully Loaded"); for (EnterpriseFeederStatusAndErrorMessagesWrapper statusAndErrors : statusAndErrorsList) { if (!statusAndErrors.getStatus().isErrorEvent()) { reportWriterService.writeFormattedMessageLine("Data file: %s", statusAndErrors.getDataFileName()); reportWriterService.writeFormattedMessageLine("Reconciliation file: %s", statusAndErrors.getReconFileName()); reportWriterService.writeFormattedMessageLine("Status: %s", statusAndErrors.getStatus().getStatusDescription()); reportWriterService.writeNewLines(1); successfulFileLoaded = true; } } if (!successfulFileLoaded) { reportWriterService.writeFormattedMessageLine("No files were successfully loaded"); } reportWriterService.writeNewLines(2); boolean unsuccessfulFileLoaded = false; reportWriterService.writeSubTitle("Files NOT Successfully Loaded"); for (EnterpriseFeederStatusAndErrorMessagesWrapper statusAndErrors : statusAndErrorsList) { if (statusAndErrors.getStatus().isErrorEvent()) { reportWriterService.writeFormattedMessageLine("Data file: %s", statusAndErrors.getDataFileName() == null ? "" : statusAndErrors.getDataFileName()); reportWriterService.writeFormattedMessageLine("Reconciliation file: %s", statusAndErrors.getReconFileName() == null ? "" : statusAndErrors.getReconFileName()); reportWriterService.writeFormattedMessageLine("Status: %s", statusAndErrors.getStatus().getStatusDescription()); reportWriterService.writeNewLines(1); unsuccessfulFileLoaded = true; } } if (!unsuccessfulFileLoaded) { reportWriterService.writeFormattedMessageLine("All files were successfully loaded"); } } protected void generateErrorAndStatisticsReport(EnterpriseFeederReportData feederReportData) { errorStatisticsReport.writeStatisticLine("LABOR LEDGER RECORDS READ %,9d", feederReportData.getNumberOfRecordsRead()); errorStatisticsReport.writeStatisticLine("LABOR LEDGER ACTUALS READ %,9d", feederReportData.getNumberOfBalanceTypeActualsRead()); errorStatisticsReport.writeStatisticLine("LABOR LEDGER ENCUMBRANCES READ %,9d", feederReportData.getNumberOfBalanceTypeEncumbranceRead()); errorStatisticsReport.writeStatisticLine("FRINGE BENEFIT ACTUALS RECORDS WRITTEN %,9d", feederReportData.getNumberOfFringeActualsGenerated()); errorStatisticsReport.writeStatisticLine("FRINGE BENEFIT ENCUMBRANCE RECORS WRITTEN %,9d", feederReportData.getNumberOfFringeEncumbrancesGenerated()); errorStatisticsReport.writeStatisticLine("MAX NUMBER OF ERRORS ALLOWED %,9d", getMaximumNumberOfErrorsAllowed()); errorStatisticsReport.writeStatisticLine("NUMBER OF ERRORS FOUND %,9d", feederReportData.getNumberOfErrorEncountered()); } /** * Retrieves the system parameter value that indicates the maximum number of * errors that are allowed to occur in benefit generation * * @return int max number of errors */ protected int getMaximumNumberOfErrorsAllowed() { String maxBenefitGenerationErrorsStr = parameterService .getParameterValueAsString(LaborEnterpriseFeedStep.class, LaborConstants.BenefitCalculation.MAX_NUMBER_OF_ERRORS_ALLOWED_PARAMETER); int maxBenefitGenerationErrors = 0; if (StringUtils.isNumeric(maxBenefitGenerationErrorsStr)) { maxBenefitGenerationErrors = Integer.parseInt(maxBenefitGenerationErrorsStr); } return maxBenefitGenerationErrors; } public void setReportWriterService(ReportWriterService reportWriterService) { this.reportWriterService = reportWriterService; } /** * @see org.kuali.kfs.sys.batch.service.impl.InitiateDirectoryImpl#getRequiredDirectoryNames() */ @Override public List<String> getRequiredDirectoryNames() { return new ArrayList<String>() {{add(getDirectoryName()); }}; } public void setParameterService(ParameterService parameterService) { this.parameterService = parameterService; } public void setConfigurationService(ConfigurationService configurationService) { this.configurationService = configurationService; } public void setErrorStatisticsReport(ReportWriterService errorStatisticsReport) { this.errorStatisticsReport = errorStatisticsReport; } }