/*
* 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;
}
}