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.module.ld.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.batch.service.EnterpriseFeederNotificationService;
030    import org.kuali.kfs.gl.batch.service.impl.RequiredFilesMissingStatus;
031    import org.kuali.kfs.gl.report.LedgerSummaryReport;
032    import org.kuali.kfs.gl.service.OriginEntryGroupService;
033    import org.kuali.kfs.gl.service.impl.EnterpriseFeederStatusAndErrorMessagesWrapper;
034    import org.kuali.kfs.module.ld.LaborConstants;
035    import org.kuali.kfs.module.ld.batch.service.EnterpriseFeederService;
036    import org.kuali.kfs.module.ld.batch.service.FileEnterpriseFeederHelperService;
037    import org.kuali.kfs.sys.Message;
038    import org.kuali.kfs.sys.service.ReportWriterService;
039    import org.kuali.rice.kns.service.DateTimeService;
040    
041    /**
042     * This class iterates through the files in the enterprise feeder staging directory, which is injected by Spring. Note: this class
043     * is NOT annotated as transactional. This allows the helper service, which is defined as transactional, to do a per-file
044     * transaction.
045     */
046    public class FileEnterpriseFeederServiceImpl implements EnterpriseFeederService {
047        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(FileEnterpriseFeederServiceImpl.class);
048    
049        private String directoryName;
050        private String laborOriginEntryDirectoryName;
051    
052        private OriginEntryGroupService originEntryGroupService;
053        private DateTimeService dateTimeService;
054        private FileEnterpriseFeederHelperService fileEnterpriseFeederHelperService;
055        private EnterpriseFeederNotificationService enterpriseFeederNotificationService;
056        private String reconciliationTableId;
057    
058        private ReportWriterService reportWriterService;
059        
060        /**
061         * Feeds file sets in the directory whose name is returned by the invocation to getDirectoryName()
062         * 
063         * @see org.kuali.kfs.gl.batch.service.EnterpriseFeederService#feed(java.lang.String)
064         */
065        public void feed(String processName, boolean performNotifications) {
066            // ensure that this feeder implementation may not be run concurrently on this JVM
067    
068            // to consider: maybe use java NIO classes to perform done file locking?
069            synchronized (FileEnterpriseFeederServiceImpl.class) {
070                if (StringUtils.isBlank(directoryName)) {
071                    throw new IllegalArgumentException("directoryName not set for FileEnterpriseFeederServiceImpl.");
072                }
073                FileFilter doneFileFilter = new SuffixFileFilter(DONE_FILE_SUFFIX);
074    
075                File enterpriseFeedFile = null;
076                String enterpriseFeedFileName = LaborConstants.BatchFileSystem.LABOR_ENTERPRISE_FEED + LaborConstants.BatchFileSystem.EXTENSION; 
077                enterpriseFeedFile = new File(laborOriginEntryDirectoryName + File.separator + enterpriseFeedFileName);
078                
079                PrintStream enterpriseFeedPs = null;
080                try {
081                    enterpriseFeedPs = new PrintStream(enterpriseFeedFile);
082                } catch (FileNotFoundException e) {
083                    LOG.error("enterpriseFeedFile doesn't exist " + enterpriseFeedFileName);
084                    throw new RuntimeException("enterpriseFeedFile doesn't exist " + enterpriseFeedFileName);
085                }
086                
087                LOG.info("New File created for enterprise feeder service run: " + enterpriseFeedFileName);
088    
089                File directory = new File(directoryName);
090                if (!directory.exists() || !directory.isDirectory()) {
091                    LOG.error("Directory doesn't exist and or it's not really a directory " + directoryName);
092                    throw new RuntimeException("Directory doesn't exist and or it's not really a directory " + directoryName);
093                }
094    
095                File[] doneFiles = directory.listFiles(doneFileFilter);
096                reorderDoneFiles(doneFiles);
097    
098                LedgerSummaryReport ledgerSummaryReport = new LedgerSummaryReport();
099    
100                List<EnterpriseFeederStatusAndErrorMessagesWrapper> statusAndErrorsList = new ArrayList<EnterpriseFeederStatusAndErrorMessagesWrapper>();
101                
102                for (File doneFile : doneFiles) {
103                    File dataFile = null;
104                    File reconFile = null;
105                    
106    
107                    EnterpriseFeederStatusAndErrorMessagesWrapper statusAndErrors = new EnterpriseFeederStatusAndErrorMessagesWrapper();
108                    statusAndErrors.setErrorMessages(new ArrayList<Message>());
109    
110                    
111                    dataFile = getDataFile(doneFile);
112                    reconFile = getReconFile(doneFile);
113    
114                    statusAndErrors.setFileNames(dataFile, reconFile, doneFile);
115    
116                    if (dataFile == null) {
117                        LOG.error("Unable to find data file for done file: " + doneFile.getAbsolutePath());
118                        statusAndErrors.getErrorMessages().add(new Message("Unable to find data file for done file: " + doneFile.getAbsolutePath(), Message.TYPE_FATAL));
119                        statusAndErrors.setStatus(new RequiredFilesMissingStatus());
120                    }
121                    if (reconFile == null) {
122                        LOG.error("Unable to find recon file for done file: " + doneFile.getAbsolutePath());
123                        statusAndErrors.getErrorMessages().add(new Message("Unable to find recon file for done file: " + doneFile.getAbsolutePath(), Message.TYPE_FATAL));
124                        statusAndErrors.setStatus(new RequiredFilesMissingStatus());
125                    }
126                        
127                    try {
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, laborOriginEntryDirectoryName + File.separator + enterpriseFeedFileName);
153                
154                String enterpriseFeedDoneFileName = enterpriseFeedFileName.replace(LaborConstants.BatchFileSystem.EXTENSION, LaborConstants.BatchFileSystem.DONE_FILE_EXTENSION);
155                File enterpriseFeedDoneFile = new File (laborOriginEntryDirectoryName + 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         * Sets the laborOriginEntryDirectoryName attribute value.
170         * @param laborOriginEntryDirectoryName The laborOriginEntryDirectoryName to set.
171         */
172        public void setLaborOriginEntryDirectoryName(String laborOriginEntryDirectoryName) {
173            this.laborOriginEntryDirectoryName = laborOriginEntryDirectoryName;
174        }
175    
176        /**
177         * Reorders the files in case there's a dependency on the order in which files are fed upon. For this implementation, the
178         * purpose is to always order files in a way such that unit testing will be predictable.
179         * 
180         * @param doneFiles
181         */
182        protected void reorderDoneFiles(File[] doneFiles) {
183            // sort the list so that the unit tests will have more predictable results
184            Arrays.sort(doneFiles);
185        }
186    
187        /**
188         * Given the doneFile, this method finds the data file corresponding to the done file
189         * 
190         * @param doneFile
191         * @return a File for the data file, or null if the file doesn't exist or is not readable
192         */
193        protected File getDataFile(File doneFile) {
194            String doneFileAbsPath = doneFile.getAbsolutePath();
195            if (!doneFileAbsPath.endsWith(DONE_FILE_SUFFIX)) {
196                LOG.error("Done file name must end with " + DONE_FILE_SUFFIX);
197                throw new IllegalArgumentException("Done file name must end with " + DONE_FILE_SUFFIX);
198            }
199            String dataFileAbsPath = StringUtils.removeEnd(doneFileAbsPath, DONE_FILE_SUFFIX) + DATA_FILE_SUFFIX;
200            File dataFile = new File(dataFileAbsPath);
201            if (!dataFile.exists() || !dataFile.canRead()) {
202                LOG.error("Cannot find/read data file " + dataFileAbsPath);
203                return null;
204            }
205            return dataFile;
206        }
207    
208        /**
209         * Given the doneFile, this method finds the reconciliation file corresponding to the data file
210         * 
211         * @param doneFile
212         * @return a file for the reconciliation data, or null if the file doesn't exist or is not readable
213         */
214        protected File getReconFile(File doneFile) {
215            String doneFileAbsPath = doneFile.getAbsolutePath();
216            if (!doneFileAbsPath.endsWith(DONE_FILE_SUFFIX)) {
217                LOG.error("Done file name must end with " + DONE_FILE_SUFFIX);
218                throw new IllegalArgumentException("DOne file name must end with " + DONE_FILE_SUFFIX);
219            }
220            String reconFileAbsPath = StringUtils.removeEnd(doneFileAbsPath, DONE_FILE_SUFFIX) + RECON_FILE_SUFFIX;
221            File reconFile = new File(reconFileAbsPath);
222            if (!reconFile.exists() || !reconFile.canRead()) {
223                LOG.error("Cannot find/read data file " + reconFileAbsPath);
224                return null;
225            }
226            return reconFile;
227        }
228    
229        /**
230         * Gets the directoryName attribute.
231         * 
232         * @return Returns the directoryName.
233         */
234        public String getDirectoryName() {
235            return directoryName;
236        }
237    
238        /**
239         * Sets the directoryName attribute value.
240         * 
241         * @param directoryName The directoryName to set.
242         */
243        public void setDirectoryName(String directoryName) {
244            this.directoryName = directoryName;
245        }
246    
247        /**
248         * Gets the originEntryGroupService attribute.
249         * 
250         * @return Returns the originEntryGroupService.
251         */
252        public OriginEntryGroupService getOriginEntryGroupService() {
253            return originEntryGroupService;
254        }
255    
256        /**
257         * Sets the originEntryGroupService attribute value.
258         * 
259         * @param originEntryGroupService The originEntryGroupService to set.
260         */
261        public void setOriginEntryGroupService(OriginEntryGroupService originEntryGroupService) {
262            this.originEntryGroupService = originEntryGroupService;
263        }
264    
265        /**
266         * Gets the dateTimeService attribute.
267         * 
268         * @return Returns the dateTimeService.
269         */
270        public DateTimeService getDateTimeService() {
271            return dateTimeService;
272        }
273    
274        /**
275         * Sets the dateTimeService attribute value.
276         * 
277         * @param dateTimeService The dateTimeService to set.
278         */
279        public void setDateTimeService(DateTimeService dateTimeService) {
280            this.dateTimeService = dateTimeService;
281        }
282    
283        /**
284         * Gets the fileEnterpriseFeederHelperService attribute.
285         * 
286         * @return Returns the fileEnterpriseFeederHelperService.
287         */
288        public FileEnterpriseFeederHelperService getFileEnterpriseFeederHelperService() {
289            return fileEnterpriseFeederHelperService;
290        }
291    
292        /**
293         * Sets the fileEnterpriseFeederHelperService attribute value.
294         * 
295         * @param fileEnterpriseFeederHelperService The fileEnterpriseFeederHelperService to set.
296         */
297        public void setFileEnterpriseFeederHelperService(FileEnterpriseFeederHelperService fileEnterpriseFeederHelperServiceImpl) {
298            this.fileEnterpriseFeederHelperService = fileEnterpriseFeederHelperServiceImpl;
299        }
300    
301        /**
302         * Gets the enterpriseFeederNotificationService attribute.
303         * 
304         * @return Returns the enterpriseFeederNotificationService.
305         */
306        public EnterpriseFeederNotificationService getEnterpriseFeederNotificationService() {
307            return enterpriseFeederNotificationService;
308        }
309    
310        /**
311         * Sets the enterpriseFeederNotificationService attribute value.
312         * 
313         * @param enterpriseFeederNotificationService The enterpriseFeederNotificationService to set.
314         */
315        public void setEnterpriseFeederNotificationService(EnterpriseFeederNotificationService enterpriseFeederNotificationService) {
316            this.enterpriseFeederNotificationService = enterpriseFeederNotificationService;
317        }
318    
319        /**
320         * Gets the reconciliationTableId attribute.
321         * 
322         * @return Returns the reconciliationTableId.
323         */
324        public String getReconciliationTableId() {
325            return reconciliationTableId;
326        }
327    
328        /**
329         * Sets the reconciliationTableId attribute value.
330         * 
331         * @param reconciliationTableId The reconciliationTableId to set.
332         */
333        public void setReconciliationTableId(String reconciliationTableId) {
334            this.reconciliationTableId = reconciliationTableId;
335        }
336    
337        protected void generateReport(List<EnterpriseFeederStatusAndErrorMessagesWrapper> statusAndErrorsList, LedgerSummaryReport report, String outputFileName) {
338            reportWriterService.writeFormattedMessageLine("Output File Name:        %s", outputFileName);
339            reportWriterService.writeNewLines(1);
340            generateFilesLoadedStatusReport(statusAndErrorsList);
341            reportWriterService.pageBreak();
342            report.writeReport(reportWriterService);
343        }
344        
345        protected void generateFilesLoadedStatusReport(List<EnterpriseFeederStatusAndErrorMessagesWrapper> statusAndErrorsList) {
346            boolean successfulFileLoaded = false;
347            reportWriterService.writeSubTitle("Files Successfully Loaded");
348            for (EnterpriseFeederStatusAndErrorMessagesWrapper statusAndErrors : statusAndErrorsList) {
349                if (!statusAndErrors.getStatus().isErrorEvent()) {
350                    reportWriterService.writeFormattedMessageLine("Data file:               %s", statusAndErrors.getDataFileName());
351                    reportWriterService.writeFormattedMessageLine("Reconciliation file:     %s", statusAndErrors.getReconFileName());
352                    reportWriterService.writeFormattedMessageLine("Status:                  %s", statusAndErrors.getStatus().getStatusDescription());
353                    reportWriterService.writeNewLines(1);
354                    
355                    successfulFileLoaded = true;
356                }
357            }
358            if (!successfulFileLoaded) {
359                reportWriterService.writeFormattedMessageLine("No files were successfully loaded");
360            }
361            
362            reportWriterService.writeNewLines(2);
363            
364            boolean unsuccessfulFileLoaded = false;
365            reportWriterService.writeSubTitle("Files NOT Successfully Loaded");
366            for (EnterpriseFeederStatusAndErrorMessagesWrapper statusAndErrors : statusAndErrorsList) {
367                if (statusAndErrors.getStatus().isErrorEvent()) {
368                    reportWriterService.writeFormattedMessageLine("Data file:               %s", statusAndErrors.getDataFileName() == null ? "" : statusAndErrors.getDataFileName());
369                    reportWriterService.writeFormattedMessageLine("Reconciliation file:     %s", statusAndErrors.getReconFileName() == null ? "" : statusAndErrors.getReconFileName());
370                    reportWriterService.writeFormattedMessageLine("Status:                  %s", statusAndErrors.getStatus().getStatusDescription());
371                    reportWriterService.writeNewLines(1);
372                    
373                    unsuccessfulFileLoaded = true;
374                }
375            }
376            if (!unsuccessfulFileLoaded) {
377                reportWriterService.writeFormattedMessageLine("All files were successfully loaded");
378            }
379            
380        }
381    
382        /**
383         * Sets the reportWriterService attribute value.
384         * @param reportWriterService The reportWriterService to set.
385         */
386        public void setReportWriterService(ReportWriterService reportWriterService) {
387            this.reportWriterService = reportWriterService;
388        }
389    }