001 /* 002 * Copyright 2011 The Kuali Foundation. 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.kfs.gl.batch.service.impl; 017 018 import java.io.File; 019 import java.io.FileFilter; 020 import java.io.FileNotFoundException; 021 import java.io.IOException; 022 import java.io.PrintStream; 023 import java.util.ArrayList; 024 import java.util.Arrays; 025 import java.util.List; 026 027 import org.apache.commons.io.filefilter.SuffixFileFilter; 028 import org.apache.commons.lang.StringUtils; 029 import org.kuali.kfs.gl.GeneralLedgerConstants; 030 import org.kuali.kfs.gl.batch.service.EnterpriseFeederNotificationService; 031 import org.kuali.kfs.gl.batch.service.EnterpriseFeederService; 032 import org.kuali.kfs.gl.batch.service.FileEnterpriseFeederHelperService; 033 import org.kuali.kfs.gl.businessobject.OriginEntryGroup; 034 import org.kuali.kfs.gl.businessobject.OriginEntrySource; 035 import org.kuali.kfs.gl.report.LedgerSummaryReport; 036 import org.kuali.kfs.gl.service.OriginEntryGroupService; 037 import org.kuali.kfs.gl.service.impl.EnterpriseFeederStatusAndErrorMessagesWrapper; 038 import org.kuali.kfs.sys.Message; 039 import org.kuali.kfs.sys.service.ReportWriterService; 040 import org.kuali.rice.kns.service.DateTimeService; 041 042 /** 043 * This class iterates through the files in the enterprise feeder staging directory, which is injected by Spring. Note: this class 044 * is NOT annotated as transactional. This allows the helper service, which is defined as transactional, to do a per-file 045 * transaction. 046 */ 047 public class FileEnterpriseFeederServiceImpl implements EnterpriseFeederService { 048 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(FileEnterpriseFeederServiceImpl.class); 049 050 private String directoryName; 051 private String glOriginEntryDirectoryName; 052 053 private OriginEntryGroupService originEntryGroupService; 054 private DateTimeService dateTimeService; 055 private FileEnterpriseFeederHelperService fileEnterpriseFeederHelperService; 056 private EnterpriseFeederNotificationService enterpriseFeederNotificationService; 057 private String reconciliationTableId; 058 059 private ReportWriterService reportWriterService; 060 061 /** 062 * Feeds file sets in the directory whose name is returned by the invocation to getDirectoryName() 063 * 064 * @see org.kuali.kfs.gl.batch.service.EnterpriseFeederService#feed(java.lang.String) 065 */ 066 public void feed(String processName, boolean performNotifications) { 067 // ensure that this feeder implementation may not be run concurrently on this JVM 068 069 // to consider: maybe use java NIO classes to perform done file locking? 070 synchronized (FileEnterpriseFeederServiceImpl.class) { 071 if (StringUtils.isBlank(directoryName)) { 072 throw new IllegalArgumentException("directoryName not set for FileEnterpriseFeederServiceImpl."); 073 } 074 FileFilter doneFileFilter = new SuffixFileFilter(DONE_FILE_SUFFIX); 075 076 File enterpriseFeedFile = null; 077 String enterpriseFeedFileName = GeneralLedgerConstants.BatchFileSystem.ENTERPRISE_FEED + GeneralLedgerConstants.BatchFileSystem.EXTENSION; 078 enterpriseFeedFile = new File(glOriginEntryDirectoryName + File.separator + enterpriseFeedFileName); 079 080 PrintStream enterpriseFeedPs = null; 081 try { 082 enterpriseFeedPs = new PrintStream(enterpriseFeedFile); 083 } catch (FileNotFoundException e) { 084 LOG.error("enterpriseFeedFile doesn't exist " + enterpriseFeedFileName); 085 throw new RuntimeException("enterpriseFeedFile doesn't exist " + enterpriseFeedFileName); 086 } 087 088 LOG.info("New File created for enterprise feeder service run: " + enterpriseFeedFileName); 089 090 File directory = new File(directoryName); 091 if (!directory.exists() || !directory.isDirectory()) { 092 LOG.error("Directory doesn't exist and or it's not really a directory " + directoryName); 093 throw new RuntimeException("Directory doesn't exist and or it's not really a directory " + directoryName); 094 } 095 096 File[] doneFiles = directory.listFiles(doneFileFilter); 097 reorderDoneFiles(doneFiles); 098 099 LedgerSummaryReport ledgerSummaryReport = new LedgerSummaryReport(); 100 101 List<EnterpriseFeederStatusAndErrorMessagesWrapper> statusAndErrorsList = new ArrayList<EnterpriseFeederStatusAndErrorMessagesWrapper>(); 102 103 for (File doneFile : doneFiles) { 104 File dataFile = null; 105 File reconFile = null; 106 107 108 EnterpriseFeederStatusAndErrorMessagesWrapper statusAndErrors = new EnterpriseFeederStatusAndErrorMessagesWrapper(); 109 statusAndErrors.setErrorMessages(new ArrayList<Message>()); 110 111 try { 112 dataFile = getDataFile(doneFile); 113 reconFile = getReconFile(doneFile); 114 115 statusAndErrors.setFileNames(dataFile, reconFile, doneFile); 116 117 if (dataFile == null) { 118 LOG.error("Unable to find data file for done file: " + doneFile.getAbsolutePath()); 119 statusAndErrors.getErrorMessages().add(new Message("Unable to find data file for done file: " + doneFile.getAbsolutePath(), Message.TYPE_FATAL)); 120 statusAndErrors.setStatus(new RequiredFilesMissingStatus()); 121 } 122 if (reconFile == null) { 123 LOG.error("Unable to find recon file for done file: " + doneFile.getAbsolutePath()); 124 statusAndErrors.getErrorMessages().add(new Message("Unable to find recon file for done file: " + doneFile.getAbsolutePath(), Message.TYPE_FATAL)); 125 statusAndErrors.setStatus(new RequiredFilesMissingStatus()); 126 } 127 128 if (dataFile != null && reconFile != null) { 129 LOG.info("Data file: " + dataFile.getAbsolutePath()); 130 LOG.info("Reconciliation File: " + reconFile.getAbsolutePath()); 131 132 fileEnterpriseFeederHelperService.feedOnFile(doneFile, dataFile, reconFile, enterpriseFeedPs, processName, reconciliationTableId, statusAndErrors, ledgerSummaryReport); 133 } 134 } 135 catch (RuntimeException e) { 136 // we need to be extremely resistant to a file load failing so that it doesn't prevent other files from loading 137 LOG.error("Caught exception when feeding done file: " + doneFile.getAbsolutePath()); 138 } 139 finally { 140 statusAndErrorsList.add(statusAndErrors); 141 boolean doneFileDeleted = doneFile.delete(); 142 if (!doneFileDeleted) { 143 statusAndErrors.getErrorMessages().add(new Message("Unable to delete done file: " + doneFile.getAbsolutePath(), Message.TYPE_FATAL)); 144 } 145 if (performNotifications) { 146 enterpriseFeederNotificationService.notifyFileFeedStatus(processName, statusAndErrors.getStatus(), doneFile, dataFile, reconFile, statusAndErrors.getErrorMessages()); 147 } 148 } 149 } 150 151 enterpriseFeedPs.close(); 152 generateReport(statusAndErrorsList, ledgerSummaryReport, glOriginEntryDirectoryName + File.separator + enterpriseFeedFileName); 153 154 String enterpriseFeedDoneFileName = enterpriseFeedFileName.replace(GeneralLedgerConstants.BatchFileSystem.EXTENSION, GeneralLedgerConstants.BatchFileSystem.DONE_FILE_EXTENSION); 155 File enterpriseFeedDoneFile = new File (glOriginEntryDirectoryName + File.separator + enterpriseFeedDoneFileName); 156 if (!enterpriseFeedDoneFile.exists()){ 157 try { 158 enterpriseFeedDoneFile.createNewFile(); 159 } catch (IOException e) { 160 LOG.error("Unable to create done file for enterprise feed output group.", e); 161 throw new RuntimeException("Unable to create done file for enterprise feed output group.", e); 162 } 163 } 164 165 } 166 } 167 168 /** 169 * Reorders the files in case there's a dependency on the order in which files are fed upon. For this implementation, the 170 * purpose is to always order files in a way such that unit testing will be predictable. 171 * 172 * @param doneFiles 173 */ 174 protected void reorderDoneFiles(File[] doneFiles) { 175 // sort the list so that the unit tests will have more predictable results 176 Arrays.sort(doneFiles); 177 } 178 179 /** 180 * Given the doneFile, this method finds the data file corresponding to the done file 181 * 182 * @param doneFile 183 * @return a File for the data file, or null if the file doesn't exist or is not readable 184 */ 185 protected File getDataFile(File doneFile) { 186 String doneFileAbsPath = doneFile.getAbsolutePath(); 187 if (!doneFileAbsPath.endsWith(DONE_FILE_SUFFIX)) { 188 LOG.error("Done file name must end with " + DONE_FILE_SUFFIX); 189 throw new IllegalArgumentException("Done file name must end with " + DONE_FILE_SUFFIX); 190 } 191 String dataFileAbsPath = StringUtils.removeEnd(doneFileAbsPath, DONE_FILE_SUFFIX) + DATA_FILE_SUFFIX; 192 File dataFile = new File(dataFileAbsPath); 193 if (!dataFile.exists() || !dataFile.canRead()) { 194 LOG.error("Cannot find/read data file " + dataFileAbsPath); 195 return null; 196 } 197 return dataFile; 198 } 199 200 /** 201 * Given the doneFile, this method finds the reconciliation file corresponding to the data file 202 * 203 * @param doneFile 204 * @return a file for the reconciliation data, or null if the file doesn't exist or is not readable 205 */ 206 protected File getReconFile(File doneFile) { 207 String doneFileAbsPath = doneFile.getAbsolutePath(); 208 if (!doneFileAbsPath.endsWith(DONE_FILE_SUFFIX)) { 209 LOG.error("Done file name must end with " + DONE_FILE_SUFFIX); 210 throw new IllegalArgumentException("DOne file name must end with " + DONE_FILE_SUFFIX); 211 } 212 String reconFileAbsPath = StringUtils.removeEnd(doneFileAbsPath, DONE_FILE_SUFFIX) + RECON_FILE_SUFFIX; 213 File reconFile = new File(reconFileAbsPath); 214 if (!reconFile.exists() || !reconFile.canRead()) { 215 LOG.error("Cannot find/read data file " + reconFileAbsPath); 216 return null; 217 } 218 return reconFile; 219 } 220 221 /** 222 * Gets the directoryName attribute. 223 * 224 * @return Returns the directoryName. 225 */ 226 public String getDirectoryName() { 227 return directoryName; 228 } 229 230 /** 231 * Sets the directoryName attribute value. 232 * 233 * @param directoryName The directoryName to set. 234 */ 235 public void setDirectoryName(String directoryName) { 236 this.directoryName = directoryName; 237 } 238 239 /** 240 * Gets the originEntryGroupService attribute. 241 * 242 * @return Returns the originEntryGroupService. 243 */ 244 public OriginEntryGroupService getOriginEntryGroupService() { 245 return originEntryGroupService; 246 } 247 248 /** 249 * Sets the originEntryGroupService attribute value. 250 * 251 * @param originEntryGroupService The originEntryGroupService to set. 252 */ 253 public void setOriginEntryGroupService(OriginEntryGroupService originEntryGroupService) { 254 this.originEntryGroupService = originEntryGroupService; 255 } 256 257 /** 258 * Gets the dateTimeService attribute. 259 * 260 * @return Returns the dateTimeService. 261 */ 262 public DateTimeService getDateTimeService() { 263 return dateTimeService; 264 } 265 266 /** 267 * Sets the dateTimeService attribute value. 268 * 269 * @param dateTimeService The dateTimeService to set. 270 */ 271 public void setDateTimeService(DateTimeService dateTimeService) { 272 this.dateTimeService = dateTimeService; 273 } 274 275 /** 276 * Gets the fileEnterpriseFeederHelperService attribute. 277 * 278 * @return Returns the fileEnterpriseFeederHelperService. 279 */ 280 public FileEnterpriseFeederHelperService getFileEnterpriseFeederHelperService() { 281 return fileEnterpriseFeederHelperService; 282 } 283 284 /** 285 * Sets the fileEnterpriseFeederHelperService attribute value. 286 * 287 * @param fileEnterpriseFeederHelperService The fileEnterpriseFeederHelperService to set. 288 */ 289 public void setFileEnterpriseFeederHelperService(FileEnterpriseFeederHelperService fileEnterpriseFeederHelperServiceImpl) { 290 this.fileEnterpriseFeederHelperService = fileEnterpriseFeederHelperServiceImpl; 291 } 292 293 /** 294 * Gets the enterpriseFeederNotificationService attribute. 295 * 296 * @return Returns the enterpriseFeederNotificationService. 297 */ 298 public EnterpriseFeederNotificationService getEnterpriseFeederNotificationService() { 299 return enterpriseFeederNotificationService; 300 } 301 302 /** 303 * Sets the enterpriseFeederNotificationService attribute value. 304 * 305 * @param enterpriseFeederNotificationService The enterpriseFeederNotificationService to set. 306 */ 307 public void setEnterpriseFeederNotificationService(EnterpriseFeederNotificationService enterpriseFeederNotificationService) { 308 this.enterpriseFeederNotificationService = enterpriseFeederNotificationService; 309 } 310 311 /** 312 * Gets the reconciliationTableId attribute. 313 * 314 * @return Returns the reconciliationTableId. 315 */ 316 public String getReconciliationTableId() { 317 return reconciliationTableId; 318 } 319 320 /** 321 * Sets the reconciliationTableId attribute value. 322 * 323 * @param reconciliationTableId The reconciliationTableId to set. 324 */ 325 public void setReconciliationTableId(String reconciliationTableId) { 326 this.reconciliationTableId = reconciliationTableId; 327 } 328 329 public void setGlOriginEntryDirectoryName(String glOriginEntryDirectoryName) { 330 this.glOriginEntryDirectoryName = glOriginEntryDirectoryName; 331 } 332 333 protected void generateReport(List<EnterpriseFeederStatusAndErrorMessagesWrapper> statusAndErrorsList, LedgerSummaryReport report, String outputFileName) { 334 reportWriterService.writeFormattedMessageLine("Output File Name: %s", outputFileName); 335 reportWriterService.writeNewLines(1); 336 generateFilesLoadedStatusReport(statusAndErrorsList); 337 reportWriterService.pageBreak(); 338 report.writeReport(reportWriterService); 339 } 340 341 protected void generateFilesLoadedStatusReport(List<EnterpriseFeederStatusAndErrorMessagesWrapper> statusAndErrorsList) { 342 boolean successfulFileLoaded = false; 343 reportWriterService.writeSubTitle("Files Successfully Loaded"); 344 for (EnterpriseFeederStatusAndErrorMessagesWrapper statusAndErrors : statusAndErrorsList) { 345 if (!statusAndErrors.getStatus().isErrorEvent()) { 346 reportWriterService.writeFormattedMessageLine("Data file: %s", statusAndErrors.getDataFileName()); 347 reportWriterService.writeFormattedMessageLine("Reconciliation file: %s", statusAndErrors.getReconFileName()); 348 reportWriterService.writeFormattedMessageLine("Status: %s", statusAndErrors.getStatus().getStatusDescription()); 349 reportWriterService.writeNewLines(1); 350 351 successfulFileLoaded = true; 352 } 353 } 354 if (!successfulFileLoaded) { 355 reportWriterService.writeFormattedMessageLine("No files were successfully loaded"); 356 } 357 358 reportWriterService.writeNewLines(2); 359 360 boolean unsuccessfulFileLoaded = false; 361 reportWriterService.writeSubTitle("Files NOT Successfully Loaded"); 362 for (EnterpriseFeederStatusAndErrorMessagesWrapper statusAndErrors : statusAndErrorsList) { 363 if (statusAndErrors.getStatus().isErrorEvent()) { 364 reportWriterService.writeFormattedMessageLine("Data file: %s", statusAndErrors.getDataFileName() == null ? "" : statusAndErrors.getDataFileName()); 365 reportWriterService.writeFormattedMessageLine("Reconciliation file: %s", statusAndErrors.getReconFileName() == null ? "" : statusAndErrors.getReconFileName()); 366 reportWriterService.writeFormattedMessageLine("Status: %s", statusAndErrors.getStatus().getStatusDescription()); 367 reportWriterService.writeNewLines(1); 368 369 unsuccessfulFileLoaded = true; 370 } 371 } 372 if (!unsuccessfulFileLoaded) { 373 reportWriterService.writeFormattedMessageLine("All files were successfully loaded"); 374 } 375 376 } 377 378 /** 379 * Sets the reportWriterService attribute value. 380 * @param reportWriterService The reportWriterService to set. 381 */ 382 public void setReportWriterService(ReportWriterService reportWriterService) { 383 this.reportWriterService = reportWriterService; 384 } 385 }