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 }