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    }