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.BufferedReader;
019    import java.io.File;
020    import java.io.FileNotFoundException;
021    import java.io.FileReader;
022    import java.io.IOException;
023    import java.io.PrintStream;
024    import java.sql.Date;
025    import java.text.NumberFormat;
026    import java.util.ArrayList;
027    import java.util.Calendar;
028    import java.util.Collection;
029    import java.util.Iterator;
030    import java.util.List;
031    import java.util.Map;
032    import java.util.StringTokenizer;
033    
034    import org.apache.commons.io.FileUtils;
035    import org.apache.commons.io.LineIterator;
036    import org.apache.commons.lang.StringUtils;
037    import org.kuali.kfs.coa.businessobject.A21SubAccount;
038    import org.kuali.kfs.coa.businessobject.Account;
039    import org.kuali.kfs.coa.businessobject.BalanceType;
040    import org.kuali.kfs.coa.service.ObjectCodeService;
041    import org.kuali.kfs.coa.service.OffsetDefinitionService;
042    import org.kuali.kfs.gl.GeneralLedgerConstants;
043    import org.kuali.kfs.gl.ObjectHelper;
044    import org.kuali.kfs.gl.batch.BatchSortUtil;
045    import org.kuali.kfs.gl.batch.ScrubberStep;
046    import org.kuali.kfs.gl.businessobject.DemergerReportData;
047    import org.kuali.kfs.gl.businessobject.OriginEntryGroup;
048    import org.kuali.kfs.gl.businessobject.OriginEntryStatistics;
049    import org.kuali.kfs.gl.businessobject.Transaction;
050    import org.kuali.kfs.gl.report.LedgerSummaryReport;
051    import org.kuali.kfs.gl.report.PreScrubberReport;
052    import org.kuali.kfs.gl.report.PreScrubberReportData;
053    import org.kuali.kfs.gl.report.TransactionListingReport;
054    import org.kuali.kfs.gl.service.OriginEntryGroupService;
055    import org.kuali.kfs.gl.service.PreScrubberService;
056    import org.kuali.kfs.gl.service.ScrubberReportData;
057    import org.kuali.kfs.gl.service.ScrubberValidator;
058    import org.kuali.kfs.module.ld.LaborConstants;
059    import org.kuali.kfs.module.ld.batch.LaborScrubberSortComparator;
060    import org.kuali.kfs.module.ld.batch.LaborScrubberStep;
061    import org.kuali.kfs.module.ld.batch.service.LaborAccountingCycleCachingService;
062    import org.kuali.kfs.module.ld.businessobject.LaborOriginEntry;
063    import org.kuali.kfs.module.ld.businessobject.LaborOriginEntryFieldUtil;
064    import org.kuali.kfs.module.ld.service.LaborOriginEntryService;
065    import org.kuali.kfs.module.ld.util.FilteringLaborOriginEntryFileIterator;
066    import org.kuali.kfs.module.ld.util.LaborOriginEntryFileIterator;
067    import org.kuali.kfs.module.ld.util.FilteringLaborOriginEntryFileIterator.LaborOriginEntryFilter;
068    import org.kuali.kfs.sys.KFSKeyConstants;
069    import org.kuali.kfs.sys.KFSPropertyConstants;
070    import org.kuali.kfs.sys.Message;
071    import org.kuali.kfs.sys.KFSParameterKeyConstants.LdParameterConstants;
072    import org.kuali.kfs.sys.batch.service.WrappingBatchService;
073    import org.kuali.kfs.sys.businessobject.UniversityDate;
074    import org.kuali.kfs.sys.context.SpringContext;
075    import org.kuali.kfs.sys.dataaccess.UniversityDateDao;
076    import org.kuali.kfs.sys.service.DocumentNumberAwareReportWriterService;
077    import org.kuali.kfs.sys.service.FlexibleOffsetAccountService;
078    import org.kuali.kfs.sys.service.ReportWriterService;
079    import org.kuali.rice.kns.service.DateTimeService;
080    import org.kuali.rice.kns.service.KualiConfigurationService;
081    import org.kuali.rice.kns.service.ParameterEvaluator;
082    import org.kuali.rice.kns.service.ParameterService;
083    import org.kuali.rice.kns.service.PersistenceService;
084    import org.kuali.rice.kns.util.KualiDecimal;
085    import org.kuali.rice.kns.util.ObjectUtils;
086    
087    /**
088     * This class has the logic for the scrubber. It is required because the scrubber process needs instance variables. Instance
089     * variables in a spring service are shared between all code calling the service. This will make sure each run of the scrubber has
090     * it's own instance variables instead of being shared.
091     */
092    public class LaborScrubberProcess {
093        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LaborScrubberProcess.class);
094    
095        // 40 spaces - used for filling in descriptions with spaces
096        private static String SPACES = "                                        ";
097    
098        /* Services required */
099        private FlexibleOffsetAccountService flexibleOffsetAccountService;
100        private LaborOriginEntryService laborOriginEntryService;
101        private OriginEntryGroupService originEntryGroupService;
102        private DateTimeService dateTimeService;
103        private OffsetDefinitionService offsetDefinitionService;
104        private ObjectCodeService objectCodeService;
105        private KualiConfigurationService kualiConfigurationService;
106        private UniversityDateDao universityDateDao;
107        private PersistenceService persistenceService;
108        private ScrubberValidator scrubberValidator;
109        private LaborAccountingCycleCachingService laborAccountingCycleCachingService;
110        private PreScrubberService laborPreScrubberService;
111        
112        private DocumentNumberAwareReportWriterService laborMainReportWriterService;
113        private DocumentNumberAwareReportWriterService laborLedgerReportWriterService;
114        private ReportWriterService laborBadBalanceTypeReportWriterService;
115        private ReportWriterService laborErrorListingReportWriterService;
116        private DocumentNumberAwareReportWriterService laborGeneratedTransactionsReportWriterService;
117        private ReportWriterService laborDemergerReportWriterService;
118        private DocumentNumberAwareReportWriterService laborPreScrubberReportWriterService;
119        private ParameterService parameterService;
120        
121        private String batchFileDirectoryName;
122    
123        enum GROUP_TYPE {
124            VALID, ERROR, EXPIRED
125        }
126    
127        /* These are all different forms of the run date for this job */
128        private Date runDate;
129        private Calendar runCal;
130        private UniversityDate universityRunDate;
131        private String offsetString;
132    
133        /*
134         * These fields are used to control whether the job was run before some set time, if so, the rundate of the job will be set to
135         * 11:59 PM of the previous day
136         */
137        private Integer cutoffHour;
138        private Integer cutoffMinute;
139        private Integer cutoffSecond;
140    
141        /* These are the output groups */
142        private OriginEntryGroup validGroup;
143        private OriginEntryGroup errorGroup;
144        private OriginEntryGroup expiredGroup;
145    
146        /* Unit Of Work info */
147        private UnitOfWorkInfo unitOfWork;
148        private KualiDecimal scrubCostShareAmount;
149        private ScrubberReportData scrubberReport;
150    
151        /* Description names */
152        private String offsetDescription;
153        private String capitalizationDescription;
154        private String liabilityDescription;
155        private String transferDescription;
156        private String costShareDescription;
157    
158        private String inputFile;
159        private String validFile;
160        private String errorFile;
161        private String expiredFile;
162    
163        /**
164         * These parameters are all the dependencies.
165         */
166        public LaborScrubberProcess(FlexibleOffsetAccountService flexibleOffsetAccountService, 
167                                    LaborAccountingCycleCachingService laborAccountingCycleCachingService, 
168                                    LaborOriginEntryService laborOriginEntryService, 
169                                    OriginEntryGroupService originEntryGroupService, 
170                                    DateTimeService dateTimeService, 
171                                    OffsetDefinitionService offsetDefinitionService, 
172                                    ObjectCodeService objectCodeService, 
173                                    KualiConfigurationService kualiConfigurationService, 
174                                    UniversityDateDao universityDateDao, 
175                                    PersistenceService persistenceService, 
176                                    ScrubberValidator scrubberValidator, 
177                                    String batchFileDirectoryName, 
178                                    DocumentNumberAwareReportWriterService laborMainReportWriterService, 
179                                    DocumentNumberAwareReportWriterService laborLedgerReportWriterService, 
180                                    ReportWriterService laborBadBalanceTypeReportWriterService, 
181                                    ReportWriterService laborErrorListingReportWriterService, 
182                                    DocumentNumberAwareReportWriterService laborGeneratedTransactionsReportWriterService, 
183                                    ReportWriterService laborDemergerReportWriterService, 
184                                    PreScrubberService laborPreScrubberService, 
185                                    DocumentNumberAwareReportWriterService laborPreScrubberReportWriterService,
186                                    ParameterService parameterService) {
187            super();
188            this.flexibleOffsetAccountService = flexibleOffsetAccountService;
189            this.laborAccountingCycleCachingService = laborAccountingCycleCachingService;
190            this.laborOriginEntryService = laborOriginEntryService;
191            this.originEntryGroupService = originEntryGroupService;
192            this.dateTimeService = dateTimeService;
193            this.offsetDefinitionService = offsetDefinitionService;
194            this.objectCodeService = objectCodeService;
195            this.kualiConfigurationService = kualiConfigurationService;
196            this.universityDateDao = universityDateDao;
197            this.persistenceService = persistenceService;
198            this.scrubberValidator = scrubberValidator;
199            this.batchFileDirectoryName = batchFileDirectoryName;
200            this.laborMainReportWriterService = laborMainReportWriterService;
201            this.laborLedgerReportWriterService = laborLedgerReportWriterService;
202            this.laborBadBalanceTypeReportWriterService = laborBadBalanceTypeReportWriterService;
203            this.laborErrorListingReportWriterService = laborErrorListingReportWriterService;
204            this.laborGeneratedTransactionsReportWriterService = laborGeneratedTransactionsReportWriterService;
205            this.laborDemergerReportWriterService = laborDemergerReportWriterService;
206            this.laborPreScrubberService = laborPreScrubberService;
207            this.laborPreScrubberReportWriterService = laborPreScrubberReportWriterService;
208            this.parameterService = parameterService;
209            
210            cutoffHour = null;
211            cutoffMinute = null;
212            cutoffSecond = null;
213    
214            initCutoffTime();
215        }
216    
217        /**
218         * Scrub this single group read only. This will only output the scrubber report. It won't output any other groups.
219         * 
220         * @param group
221         */
222        public void scrubGroupReportOnly(String fileName, String documentNumber) {
223            String unsortedFile = fileName;
224            this.inputFile = fileName + ".sort";
225            this.validFile = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.SCRUBBER_VALID_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
226            this.errorFile = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.SCRUBBER_ERROR_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
227            this.expiredFile = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.SCRUBBER_EXPIRED_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
228            String prescrubOutput = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.PRE_SCRUBBER_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
229            
230            PreScrubberReportData preScrubberReportData = null;
231            // run pre-scrubber on the raw input into the sort process
232            LineIterator inputEntries = null;
233            try {
234                inputEntries = FileUtils.lineIterator(new File(unsortedFile));
235                preScrubberReportData = laborPreScrubberService.preprocessOriginEntries(inputEntries, prescrubOutput);
236            }
237            catch (IOException e1) {
238                LOG.error("Error encountered trying to prescrub GLCP/LLCP document", e1);
239                throw new RuntimeException("Error encountered trying to prescrub GLCP/LLCP document", e1);
240            }
241            finally {
242                LineIterator.closeQuietly(inputEntries);
243            }
244            if (preScrubberReportData != null) {
245                laborPreScrubberReportWriterService.setDocumentNumber(documentNumber);
246                ((WrappingBatchService)laborPreScrubberReportWriterService).initialize();
247                try {
248                    new PreScrubberReport().generateReport(preScrubberReportData, laborPreScrubberReportWriterService);
249                }
250                finally {
251                    ((WrappingBatchService)laborPreScrubberReportWriterService).destroy();
252                }
253            }
254            BatchSortUtil.sortTextFileWithFields(prescrubOutput, inputFile, new LaborScrubberSortComparator());
255    
256            scrubEntries(true, documentNumber);
257    
258            File deleteSortFile = new File(inputFile);
259            File deleteValidFile = new File(validFile);
260            File deleteErrorFile = new File(errorFile);
261            File deleteExpiredFile = new File(expiredFile);
262            try {
263                deleteSortFile.delete();
264                deleteValidFile.delete();
265                deleteErrorFile.delete();
266                deleteExpiredFile.delete();
267            }
268            catch (Exception e) {
269                LOG.error("scrubGroupReportOnly delete output files process Stopped: " + e.getMessage());
270                throw new RuntimeException("scrubGroupReportOnly delete output files process Stopped: " + e.getMessage(), e);
271            }
272        }
273    
274        public void scrubEntries() {
275            this.inputFile = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.SCRUBBER_INPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
276            this.validFile = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.SCRUBBER_VALID_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
277            this.errorFile = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.SCRUBBER_ERROR_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
278            this.expiredFile = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.SCRUBBER_EXPIRED_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
279    
280            scrubEntries(false, null);
281        }
282    
283        /**
284         * Scrub all entries that need it in origin entry. Put valid scrubbed entries in a scrubber valid group, put errors in a
285         * scrubber error group, and transactions with an expired account in the scrubber expired account group.
286         */
287        public void scrubEntries(boolean reportOnlyMode, String documentNumber) {
288            LOG.debug("scrubEntries() started");
289    
290            if (reportOnlyMode) {
291                laborMainReportWriterService.setDocumentNumber(documentNumber);
292                laborLedgerReportWriterService.setDocumentNumber(documentNumber);
293                laborGeneratedTransactionsReportWriterService.setDocumentNumber(documentNumber);
294            }
295    
296            // setup an object to hold the "default" date information
297            runDate = calculateRunDate(dateTimeService.getCurrentDate());
298            runCal = Calendar.getInstance();
299            runCal.setTime(runDate);
300    
301            universityRunDate = laborAccountingCycleCachingService.getUniversityDate(runDate);
302            if (universityRunDate == null) {
303                throw new IllegalStateException(kualiConfigurationService.getPropertyString(KFSKeyConstants.ERROR_UNIV_DATE_NOT_FOUND));
304            }
305            setOffsetString();
306            setDescriptions();
307    
308            try {
309                ((WrappingBatchService) laborMainReportWriterService).initialize();
310                ((WrappingBatchService) laborLedgerReportWriterService).initialize();
311                if (reportOnlyMode) {
312                    ((WrappingBatchService) laborGeneratedTransactionsReportWriterService).initialize();
313                }
314    
315                scrubberReport = new ScrubberReportData();
316                processGroup();
317    
318                // Run the reports
319                if (reportOnlyMode) {
320                    generateScrubberTransactionsOnline();
321                }
322                else {
323                    generateScrubberBadBalanceTypeListingReport();
324                }
325            }
326            finally {
327                ((WrappingBatchService) laborMainReportWriterService).destroy();
328                ((WrappingBatchService) laborLedgerReportWriterService).destroy();
329                if (reportOnlyMode) {
330                    ((WrappingBatchService) laborGeneratedTransactionsReportWriterService).destroy();
331                }
332            }
333        }
334    
335        /**
336         * Determine the type of the transaction by looking at attributes
337         * 
338         * @param transaction Transaction to identify
339         * @return CE (Cost share encumbrance, O (Offset), C (apitalization), L (Liability), T (Transfer), CS (Cost Share), X (Other)
340         */
341        protected String getTransactionType(LaborOriginEntry transaction) {
342            if ("CE".equals(transaction.getFinancialBalanceTypeCode())) {
343                return "CE";
344            }
345            String desc = transaction.getTransactionLedgerEntryDescription();
346    
347            if (desc == null) {
348                return "X";
349            }
350    
351            if (desc.startsWith(offsetDescription) && desc.indexOf("***") > -1) {
352                return "CS";
353            }
354            if (desc.startsWith(costShareDescription) && desc.indexOf("***") > -1) {
355                return "CS";
356            }
357            if (desc.startsWith(offsetDescription)) {
358                return "O";
359            }
360            if (desc.startsWith(capitalizationDescription)) {
361                return "C";
362            }
363            if (desc.startsWith(liabilityDescription)) {
364                return "L";
365            }
366            if (desc.startsWith(transferDescription)) {
367                return "T";
368            }
369            return "X";
370        }
371    
372        /**
373         * This will process a group of origin entries. The COBOL code was refactored a lot to get this so there isn't a 1 to 1 section
374         * of Cobol relating to this.
375         * 
376         * @param originEntryGroup Group to process
377         */
378        protected void processGroup() {
379            ParameterService parameterService = SpringContext.getBean(ParameterService.class);
380            LaborOriginEntry lastEntry = null;
381            scrubCostShareAmount = KualiDecimal.ZERO;
382            unitOfWork = new UnitOfWorkInfo();
383            FileReader INPUT_GLE_FILE = null;
384            String GLEN_RECORD;
385            BufferedReader INPUT_GLE_FILE_br;
386            PrintStream OUTPUT_GLE_FILE_ps;
387            PrintStream OUTPUT_ERR_FILE_ps;
388            PrintStream OUTPUT_EXP_FILE_ps;
389            try {
390                INPUT_GLE_FILE = new FileReader(inputFile);
391            }
392            catch (FileNotFoundException e) {
393                throw new RuntimeException("Unable to find input file: " + inputFile, e);
394            }
395            try {
396                OUTPUT_GLE_FILE_ps = new PrintStream(validFile);
397                OUTPUT_ERR_FILE_ps = new PrintStream(errorFile);
398                OUTPUT_EXP_FILE_ps = new PrintStream(expiredFile);
399            }
400            catch (IOException e) {
401                throw new RuntimeException("Problem opening output files", e);
402            }
403    
404            INPUT_GLE_FILE_br = new BufferedReader(INPUT_GLE_FILE);
405            LOG.info("Starting Scrubber Process process group...");
406    
407            int lineNumber = 0;
408            int loadedCount = 0;
409            boolean errorsLoading = false;
410    
411            LedgerSummaryReport laborLedgerSummaryReport = new LedgerSummaryReport();
412            LaborOriginEntry unscrubbedEntry = new LaborOriginEntry();
413            List<Message> tmperrors = new ArrayList<Message>();
414            try {
415                String currentLine = INPUT_GLE_FILE_br.readLine();
416    
417                while (currentLine != null) {
418                    boolean saveErrorTransaction = false;
419                    boolean saveValidTransaction = false;
420                    LaborOriginEntry scrubbedEntry = new LaborOriginEntry();
421                    try {
422                        lineNumber++;
423    
424                        if (!StringUtils.isEmpty(currentLine) && !StringUtils.isBlank(currentLine.trim())) {
425                            unscrubbedEntry = new LaborOriginEntry();
426                            tmperrors = unscrubbedEntry.setFromTextFileForBatch(currentLine, lineNumber);
427                            loadedCount++;
428    
429                            // just test entry with the entry loaded above
430                            scrubberReport.incrementUnscrubbedRecordsRead();
431                            List<Message> transactionErrors = new ArrayList<Message>();
432    
433                            // This is done so if the code modifies this row, then saves it, it will be an insert,
434                            // and it won't touch the original. The Scrubber never modifies input rows/groups.
435                            unscrubbedEntry.setGroup(null);
436                            unscrubbedEntry.setVersionNumber(null);
437                            unscrubbedEntry.setEntryId(null);
438                            saveErrorTransaction = false;
439                            saveValidTransaction = false;
440    
441                            // Build a scrubbed entry
442                            // Labor has more fields
443                            buildScrubbedEntry(unscrubbedEntry, scrubbedEntry);
444    
445                            // For Labor Scrubber
446                            boolean laborIndicator = true;
447                            laborLedgerSummaryReport.summarizeEntry(unscrubbedEntry);
448    
449                            try {
450                                tmperrors.addAll(scrubberValidator.validateTransaction(unscrubbedEntry, scrubbedEntry, universityRunDate, laborIndicator, laborAccountingCycleCachingService));
451                            }
452                            catch (Exception e) {
453                                transactionErrors.add(new Message(e.toString() + " occurred for this record.", Message.TYPE_FATAL));
454                                saveValidTransaction = false;
455                            }
456                            transactionErrors.addAll(tmperrors);
457    
458                            // Expired account?
459                            Account unscrubbedEntryAccount = laborAccountingCycleCachingService.getAccount(unscrubbedEntry.getChartOfAccountsCode(), unscrubbedEntry.getAccountNumber());
460                            if (ObjectUtils.isNotNull(unscrubbedEntry.getAccount()) && (scrubberValidator.isAccountExpired(unscrubbedEntryAccount, universityRunDate) || unscrubbedEntryAccount.isClosed())) {
461                                // Make a copy of it so OJB doesn't just update the row in the original
462                                // group. It needs to make a new one in the expired group
463                                LaborOriginEntry expiredEntry = new LaborOriginEntry(scrubbedEntry);
464    
465                                createOutputEntry(expiredEntry, OUTPUT_EXP_FILE_ps);
466                                scrubberReport.incrementExpiredAccountFound();
467                            }
468    
469                            if (!isFatal(transactionErrors)) {
470                                saveValidTransaction = true;
471    
472                                // See if unit of work has changed
473                                if (!unitOfWork.isSameUnitOfWork(scrubbedEntry)) {
474                                    // Generate offset for last unit of work
475                                    unitOfWork = new UnitOfWorkInfo(scrubbedEntry);
476                                }
477                                KualiDecimal transactionAmount = scrubbedEntry.getTransactionLedgerEntryAmount();
478                                ParameterEvaluator offsetFiscalPeriods = SpringContext.getBean(ParameterService.class).getParameterEvaluator(ScrubberStep.class, GeneralLedgerConstants.GlScrubberGroupRules.OFFSET_FISCAL_PERIOD_CODES, scrubbedEntry.getUniversityFiscalPeriodCode());
479                                BalanceType scrubbedEntryBalanceType = laborAccountingCycleCachingService.getBalanceType(scrubbedEntry.getFinancialBalanceTypeCode());
480                                if (scrubbedEntryBalanceType.isFinancialOffsetGenerationIndicator() && offsetFiscalPeriods.evaluationSucceeds()) {
481                                    if (scrubbedEntry.isDebit()) {
482                                        unitOfWork.offsetAmount = unitOfWork.offsetAmount.add(transactionAmount);
483                                    }
484                                    else {
485                                        unitOfWork.offsetAmount = unitOfWork.offsetAmount.subtract(transactionAmount);
486                                    }
487                                }
488    
489                                // The sub account type code will only exist if there is a valid sub account
490                                // TODO: GLConstants.getSpaceSubAccountTypeCode();
491                                String subAccountTypeCode = "  ";
492    
493                                A21SubAccount scrubbedEntryA21SubAccount = laborAccountingCycleCachingService.getA21SubAccount(scrubbedEntry.getChartOfAccountsCode(), scrubbedEntry.getAccountNumber(), scrubbedEntry.getSubAccountNumber());
494                                if (ObjectUtils.isNotNull(scrubbedEntryA21SubAccount)) {
495                                    subAccountTypeCode = scrubbedEntryA21SubAccount.getSubAccountTypeCode();
496                                }
497    
498                                if (transactionErrors.size() > 0) {
499                                    this.laborMainReportWriterService.writeError(unscrubbedEntry, transactionErrors);
500                                }
501    
502                                lastEntry = scrubbedEntry;
503                            }
504                            else {
505                                // Error transaction
506                                saveErrorTransaction = true;
507                                this.laborMainReportWriterService.writeError(unscrubbedEntry, transactionErrors);
508                            }
509    
510      
511                            if (saveValidTransaction) {
512                                scrubbedEntry.setTransactionScrubberOffsetGenerationIndicator(false);
513                                createOutputEntry(scrubbedEntry, OUTPUT_GLE_FILE_ps);
514                                scrubberReport.incrementScrubbedRecordWritten();
515                            }
516    
517                            if (saveErrorTransaction) {
518                                // Make a copy of it so OJB doesn't just update the row in the original
519                                // group. It needs to make a new one in the error group
520                                LaborOriginEntry errorEntry = new LaborOriginEntry(unscrubbedEntry);
521                                errorEntry.setTransactionScrubberOffsetGenerationIndicator(false);
522                                createOutputEntry(currentLine, OUTPUT_ERR_FILE_ps);
523                                scrubberReport.incrementErrorRecordWritten();
524                            }
525                        }
526                        currentLine = INPUT_GLE_FILE_br.readLine();
527    
528                    }
529                    catch (IOException ioe) {
530                        // catch here again, it should be from postSingleEntryIntoLaborLedger
531                        LOG.error("processGroup() stopped due to: " + ioe.getMessage() + " on line number : " + loadedCount, ioe);
532                        throw new RuntimeException("processGroup() stopped due to: " + ioe.getMessage() + " on line number : " + loadedCount, ioe);
533                    }
534                }
535                INPUT_GLE_FILE_br.close();
536                INPUT_GLE_FILE.close();
537                OUTPUT_GLE_FILE_ps.close();
538                OUTPUT_ERR_FILE_ps.close();
539                OUTPUT_EXP_FILE_ps.close();
540    
541                this.laborMainReportWriterService.writeStatisticLine("UNSCRUBBED RECORDS READ              %,9d", scrubberReport.getNumberOfUnscrubbedRecordsRead());
542                this.laborMainReportWriterService.writeStatisticLine("SCRUBBED RECORDS WRITTEN             %,9d", scrubberReport.getNumberOfScrubbedRecordsWritten());
543                this.laborMainReportWriterService.writeStatisticLine("ERROR RECORDS WRITTEN                %,9d", scrubberReport.getNumberOfErrorRecordsWritten());
544                this.laborMainReportWriterService.writeStatisticLine("TOTAL OUTPUT RECORDS WRITTEN         %,9d", scrubberReport.getTotalNumberOfRecordsWritten());
545                this.laborMainReportWriterService.writeStatisticLine("EXPIRED ACCOUNTS FOUND               %,9d", scrubberReport.getNumberOfExpiredAccountsFound());
546    
547                laborLedgerSummaryReport.writeReport(this.laborLedgerReportWriterService);
548            }
549            catch (IOException ioe) {
550                LOG.error("processGroup() stopped due to: " + ioe.getMessage(), ioe);
551                throw new RuntimeException("processGroup() stopped due to: " + ioe.getMessage(), ioe);
552            }
553        }
554    
555    
556        protected boolean isFatal(List<Message> errors) {
557            for (Iterator<Message> iter = errors.iterator(); iter.hasNext();) {
558                Message element = iter.next();
559                if (element.getType() == Message.TYPE_FATAL) {
560                    return true;
561                }
562            }
563            return false;
564        }
565    
566        /**
567         * Get all the transaction descriptions from the param table
568         */
569        protected void setDescriptions() {
570            offsetDescription = kualiConfigurationService.getPropertyString(KFSKeyConstants.MSG_GENERATED_OFFSET);
571            capitalizationDescription = kualiConfigurationService.getPropertyString(KFSKeyConstants.MSG_GENERATED_CAPITALIZATION);
572            liabilityDescription = kualiConfigurationService.getPropertyString(KFSKeyConstants.MSG_GENERATED_LIABILITY);
573            costShareDescription = kualiConfigurationService.getPropertyString(KFSKeyConstants.MSG_GENERATED_COST_SHARE);
574            transferDescription = kualiConfigurationService.getPropertyString(KFSKeyConstants.MSG_GENERATED_TRANSFER);
575        }
576    
577        /**
578         * Generate the flag for the end of specific descriptions. This will be used in the demerger step
579         */
580        protected void setOffsetString() {
581    
582            NumberFormat nf = NumberFormat.getInstance();
583            nf.setMaximumFractionDigits(0);
584            nf.setMaximumIntegerDigits(2);
585            nf.setMinimumFractionDigits(0);
586            nf.setMinimumIntegerDigits(2);
587    
588            offsetString = "***" + nf.format(runCal.get(Calendar.MONTH) + 1) + nf.format(runCal.get(Calendar.DAY_OF_MONTH));
589        }
590    
591        /**
592         * Generate the offset message with the flag at the end
593         * 
594         * @return Offset message
595         */
596        protected String getOffsetMessage() {
597            String msg = offsetDescription + SPACES;
598    
599            return msg.substring(0, 33) + offsetString;
600        }
601    
602        class UnitOfWorkInfo {
603            // Unit of work key
604            public Integer univFiscalYr = 0;
605            public String finCoaCd = "";
606            public String accountNbr = "";
607            public String subAcctNbr = "";
608            public String finBalanceTypCd = "";
609            public String fdocTypCd = "";
610            public String fsOriginCd = "";
611            public String fdocNbr = "";
612            public Date fdocReversalDt = new Date(dateTimeService.getCurrentDate().getTime());
613            public String univFiscalPrdCd = "";
614    
615            // Data about unit of work
616            public boolean entryMode = true;
617            public KualiDecimal offsetAmount = KualiDecimal.ZERO;
618            public String scrbFinCoaCd;
619            public String scrbAccountNbr;
620    
621            public UnitOfWorkInfo() {
622            }
623    
624            public UnitOfWorkInfo(LaborOriginEntry e) {
625                univFiscalYr = e.getUniversityFiscalYear();
626                finCoaCd = e.getChartOfAccountsCode();
627                accountNbr = e.getAccountNumber();
628                subAcctNbr = e.getSubAccountNumber();
629                finBalanceTypCd = e.getFinancialBalanceTypeCode();
630                fdocTypCd = e.getFinancialDocumentTypeCode();
631                fsOriginCd = e.getFinancialSystemOriginationCode();
632                fdocNbr = e.getDocumentNumber();
633                fdocReversalDt = e.getFinancialDocumentReversalDate();
634                univFiscalPrdCd = e.getUniversityFiscalPeriodCode();
635            }
636    
637            public boolean isSameUnitOfWork(LaborOriginEntry e) {
638                // Compare the key fields
639                return univFiscalYr.equals(e.getUniversityFiscalYear()) && finCoaCd.equals(e.getChartOfAccountsCode()) && accountNbr.equals(e.getAccountNumber()) && subAcctNbr.equals(e.getSubAccountNumber()) && finBalanceTypCd.equals(e.getFinancialBalanceTypeCode()) && fdocTypCd.equals(e.getFinancialDocumentTypeCode()) && fsOriginCd.equals(e.getFinancialSystemOriginationCode()) && fdocNbr.equals(e.getDocumentNumber()) && ObjectHelper.isEqual(fdocReversalDt, e.getFinancialDocumentReversalDate()) && univFiscalPrdCd.equals(e.getUniversityFiscalPeriodCode());
640            }
641    
642            public String toString() {
643                return univFiscalYr + finCoaCd + accountNbr + subAcctNbr + finBalanceTypCd + fdocTypCd + fsOriginCd + fdocNbr + fdocReversalDt + univFiscalPrdCd;
644            }
645    
646            public LaborOriginEntry getOffsetTemplate() {
647                LaborOriginEntry e = new LaborOriginEntry();
648                e.setUniversityFiscalYear(univFiscalYr);
649                e.setChartOfAccountsCode(finCoaCd);
650                e.setAccountNumber(accountNbr);
651                e.setSubAccountNumber(subAcctNbr);
652                e.setFinancialBalanceTypeCode(finBalanceTypCd);
653                e.setFinancialDocumentTypeCode(fdocTypCd);
654                e.setFinancialSystemOriginationCode(fsOriginCd);
655                e.setDocumentNumber(fdocNbr);
656                e.setFinancialDocumentReversalDate(fdocReversalDt);
657                e.setUniversityFiscalPeriodCode(univFiscalPrdCd);
658                return e;
659            }
660        }
661    
662        class TransactionError {
663            public Transaction transaction;
664            public Message message;
665    
666            public TransactionError(Transaction t, Message m) {
667                transaction = t;
668                message = m;
669            }
670        }
671    
672        protected void setCutoffTimeForPreviousDay(int hourOfDay, int minuteOfDay, int secondOfDay) {
673            this.cutoffHour = hourOfDay;
674            this.cutoffMinute = minuteOfDay;
675            this.cutoffSecond = secondOfDay;
676    
677            LOG.info("Setting cutoff time to hour: " + hourOfDay + ", minute: " + minuteOfDay + ", second: " + secondOfDay);
678        }
679    
680        protected void setCutoffTime(String cutoffTime) {
681            if (StringUtils.isBlank(cutoffTime)) {
682                LOG.debug("Cutoff time is blank");
683                unsetCutoffTimeForPreviousDay();
684            }
685            else {
686                cutoffTime = cutoffTime.trim();
687                LOG.debug("Cutoff time value found: " + cutoffTime);
688                StringTokenizer st = new StringTokenizer(cutoffTime, ":", false);
689    
690                try {
691                    String hourStr = st.nextToken();
692                    String minuteStr = st.nextToken();
693                    String secondStr = st.nextToken();
694    
695                    int hourInt = Integer.parseInt(hourStr, 10);
696                    int minuteInt = Integer.parseInt(minuteStr, 10);
697                    int secondInt = Integer.parseInt(secondStr, 10);
698    
699                    if (hourInt < 0 || hourInt > 23 || minuteInt < 0 || minuteInt > 59 || secondInt < 0 || secondInt > 59) {
700                        throw new IllegalArgumentException("Cutoff time must be in the format \"HH:mm:ss\", where HH, mm, ss are defined in the java.text.SimpleDateFormat class.  In particular, 0 <= hour <= 23, 0 <= minute <= 59, and 0 <= second <= 59");
701                    }
702                    setCutoffTimeForPreviousDay(hourInt, minuteInt, secondInt);
703                }
704                catch (Exception e) {
705                    throw new IllegalArgumentException("Cutoff time should either be null, or in the format \"HH:mm:ss\", where HH, mm, ss are defined in the java.text.SimpleDateFormat class.",e);
706                }
707            }
708        }
709    
710    
711        public void unsetCutoffTimeForPreviousDay() {
712            this.cutoffHour = null;
713            this.cutoffMinute = null;
714            this.cutoffSecond = null;
715        }
716    
717        /**
718         * This method modifies the run date if it is before the cutoff time specified by calling the setCutoffTimeForPreviousDay
719         * method. See KULRNE-70 This method is public to facilitate unit testing
720         * 
721         * @param currentDate
722         * @return
723         */
724        public java.sql.Date calculateRunDate(java.util.Date currentDate) {
725            Calendar currentCal = Calendar.getInstance();
726            currentCal.setTime(currentDate);
727    
728            if (isCurrentDateBeforeCutoff(currentCal)) {
729                // time to set the date to the previous day's last minute/second
730                currentCal.add(Calendar.DAY_OF_MONTH, -1);
731                // per old COBOL code (see KULRNE-70),
732                // the time is set to 23:59:59 (assuming 0 ms)
733                currentCal.set(Calendar.HOUR_OF_DAY, 23);
734                currentCal.set(Calendar.MINUTE, 59);
735                currentCal.set(Calendar.SECOND, 59);
736                currentCal.set(Calendar.MILLISECOND, 0);
737                return new java.sql.Date(currentCal.getTimeInMillis());
738            }
739            return new java.sql.Date(currentDate.getTime());
740        }
741    
742        protected boolean isCurrentDateBeforeCutoff(Calendar currentCal) {
743            if (cutoffHour != null && cutoffMinute != null && cutoffSecond != null) {
744                // if cutoff date is not properly defined
745                // 24 hour clock (i.e. hour is 0 - 23)
746    
747                // clone the calendar so we get the same month, day, year
748                // then change the hour, minute, second fields
749                // then see if the cutoff is before or after
750                Calendar cutoffTime = (Calendar) currentCal.clone();
751                cutoffTime.setLenient(false);
752                cutoffTime.set(Calendar.HOUR_OF_DAY, cutoffHour);
753                cutoffTime.set(Calendar.MINUTE, cutoffMinute);
754                cutoffTime.set(Calendar.SECOND, cutoffSecond);
755                cutoffTime.set(Calendar.MILLISECOND, 0);
756    
757                return currentCal.before(cutoffTime);
758            }
759            // if cutoff date is not properly defined, then it is considered to be after the cutoff
760            return false;
761        }
762    
763        protected void initCutoffTime() {
764            String cutoffTime = SpringContext.getBean(ParameterService.class).getParameterValue(ScrubberStep.class, GeneralLedgerConstants.GlScrubberGroupParameters.SCRUBBER_CUTOFF_TIME);
765            if (StringUtils.isBlank(cutoffTime)) {
766                LOG.debug("Cutoff time system parameter not found");
767                unsetCutoffTimeForPreviousDay();
768                return;
769            }
770            setCutoffTime(cutoffTime);
771        }
772    
773        protected void buildScrubbedEntry(LaborOriginEntry unscrubbedEntry, LaborOriginEntry scrubbedEntry) {
774            scrubbedEntry.setDocumentNumber(unscrubbedEntry.getDocumentNumber());
775            scrubbedEntry.setOrganizationDocumentNumber(unscrubbedEntry.getOrganizationDocumentNumber());
776            scrubbedEntry.setOrganizationReferenceId(unscrubbedEntry.getOrganizationReferenceId());
777            scrubbedEntry.setReferenceFinancialDocumentNumber(unscrubbedEntry.getReferenceFinancialDocumentNumber());
778    
779            Integer transactionNumber = unscrubbedEntry.getTransactionLedgerEntrySequenceNumber();
780            scrubbedEntry.setTransactionLedgerEntrySequenceNumber(null == transactionNumber ? new Integer(0) : transactionNumber);
781            scrubbedEntry.setTransactionLedgerEntryDescription(unscrubbedEntry.getTransactionLedgerEntryDescription());
782            scrubbedEntry.setTransactionLedgerEntryAmount(unscrubbedEntry.getTransactionLedgerEntryAmount());
783            scrubbedEntry.setTransactionDebitCreditCode(unscrubbedEntry.getTransactionDebitCreditCode());
784    
785            // For Labor's more fields
786            // It might be changed based on Labor Scrubber's business rule
787            scrubbedEntry.setPositionNumber(unscrubbedEntry.getPositionNumber());
788            scrubbedEntry.setTransactionPostingDate(unscrubbedEntry.getTransactionPostingDate());
789            scrubbedEntry.setPayPeriodEndDate(unscrubbedEntry.getPayPeriodEndDate());
790            scrubbedEntry.setTransactionTotalHours(unscrubbedEntry.getTransactionTotalHours());
791            scrubbedEntry.setPayrollEndDateFiscalYear(unscrubbedEntry.getPayrollEndDateFiscalYear());
792            scrubbedEntry.setPayrollEndDateFiscalPeriodCode(unscrubbedEntry.getPayrollEndDateFiscalPeriodCode());
793            scrubbedEntry.setFinancialDocumentApprovedCode(unscrubbedEntry.getFinancialDocumentApprovedCode());
794            scrubbedEntry.setTransactionEntryOffsetCode(unscrubbedEntry.getTransactionEntryOffsetCode());
795            scrubbedEntry.setTransactionEntryProcessedTimestamp(unscrubbedEntry.getTransactionEntryProcessedTimestamp());
796            scrubbedEntry.setEmplid(unscrubbedEntry.getEmplid());
797            scrubbedEntry.setEmployeeRecord(unscrubbedEntry.getEmployeeRecord());
798            scrubbedEntry.setEarnCode(unscrubbedEntry.getEarnCode());
799            scrubbedEntry.setPayGroup(unscrubbedEntry.getPayGroup());
800            scrubbedEntry.setSalaryAdministrationPlan(unscrubbedEntry.getSalaryAdministrationPlan());
801            scrubbedEntry.setGrade(unscrubbedEntry.getGrade());
802            scrubbedEntry.setRunIdentifier(unscrubbedEntry.getRunIdentifier());
803            scrubbedEntry.setLaborLedgerOriginalChartOfAccountsCode(unscrubbedEntry.getLaborLedgerOriginalChartOfAccountsCode());
804            scrubbedEntry.setLaborLedgerOriginalAccountNumber(unscrubbedEntry.getLaborLedgerOriginalAccountNumber());
805            scrubbedEntry.setLaborLedgerOriginalSubAccountNumber(unscrubbedEntry.getLaborLedgerOriginalSubAccountNumber());
806            scrubbedEntry.setLaborLedgerOriginalFinancialObjectCode(unscrubbedEntry.getLaborLedgerOriginalFinancialObjectCode());
807            scrubbedEntry.setLaborLedgerOriginalFinancialSubObjectCode(unscrubbedEntry.getLaborLedgerOriginalFinancialSubObjectCode());
808            scrubbedEntry.setHrmsCompany(unscrubbedEntry.getHrmsCompany());
809            scrubbedEntry.setSetid(unscrubbedEntry.getSetid());
810            scrubbedEntry.setTransactionDateTimeStamp(unscrubbedEntry.getTransactionDateTimeStamp());
811            scrubbedEntry.setReferenceFinancialDocumentTypeCode(unscrubbedEntry.getReferenceFinancialDocumentTypeCode());
812            scrubbedEntry.setReferenceFinancialSystemOrigination(unscrubbedEntry.getReferenceFinancialSystemOrigination());
813            scrubbedEntry.setPayrollEndDateFiscalPeriod(unscrubbedEntry.getPayrollEndDateFiscalPeriod());
814        }
815    
816        /**
817         * The demerger process reads all of the documents in the error group, then moves all of the original entries for that document
818         * from the valid group to the error group. It does not move generated entries to the error group. Those are deleted. It also
819         * modifies the doc number and origin code of cost share transfers.
820         * 
821         * @param errorGroup
822         * @param validGroup
823         */
824        public void performDemerger() {
825            LOG.debug("performDemerger() started");
826            LaborOriginEntryFieldUtil loefu = new LaborOriginEntryFieldUtil();
827            Map<String, Integer> pMap = loefu.getFieldBeginningPositionMap();
828            
829            String validOutputFilename = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.SCRUBBER_VALID_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
830            String errorOutputFilename = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.SCRUBBER_ERROR_SORTED_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
831            runDate = calculateRunDate(dateTimeService.getCurrentDate());
832    
833            // Without this step, the job fails with Optimistic Lock Exceptions
834            persistenceService.clearCache();
835    
836            DemergerReportData demergerReport = new DemergerReportData();
837    
838            OriginEntryStatistics eOes = laborOriginEntryService.getStatistics(errorOutputFilename);
839            demergerReport.setErrorTransactionsRead(eOes.getRowCount());
840            
841            //
842            // Get the list of document type codes from the parameter.  If the 
843            // current document type matches any of parameter defined type codes,
844            // then demerge all other entries for this document; otherwise, only
845            // pull the entry with the error and don't demerge anything else.
846            //
847            List<String> demergeDocumentTypes = parameterService.getParameterValues(
848                    LaborScrubberStep.class,
849                    LdParameterConstants.DEMERGE_DOCUMENT_TYPES);
850    
851            // Read all the documents from the error group and move all non-generated
852            // transactions for these documents from the valid group into the error group
853    
854            FileReader INPUT_GLE_FILE = null;
855            FileReader INPUT_ERR_FILE = null;
856            BufferedReader INPUT_GLE_FILE_br;
857            BufferedReader INPUT_ERR_FILE_br;
858            PrintStream OUTPUT_DEMERGER_GLE_FILE_ps;
859            PrintStream OUTPUT_DEMERGER_ERR_FILE_ps;
860            PrintStream reportPrintStream;
861    
862            String demergerValidOutputFilename = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.DEMERGER_VAILD_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
863            String demergerErrorOutputFilename = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.DEMERGER_ERROR_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
864    
865            try {
866                INPUT_GLE_FILE = new FileReader(validOutputFilename);
867                INPUT_ERR_FILE = new FileReader(errorOutputFilename);
868            }
869            catch (FileNotFoundException e) {
870                throw new RuntimeException("Unable to open input files", e);
871            }
872            try {
873                OUTPUT_DEMERGER_GLE_FILE_ps = new PrintStream(demergerValidOutputFilename);
874                OUTPUT_DEMERGER_ERR_FILE_ps = new PrintStream(demergerErrorOutputFilename);
875            }
876            catch (IOException e) {
877                throw new RuntimeException("Unable to open output files",e);
878            }
879    
880            Collection<LaborOriginEntry> validEntryCollection = new ArrayList<LaborOriginEntry>();
881    
882            int validRead = 0;
883            int errorRead = 0;
884    
885            int validSaved = 0;
886            int errorSaved = 0;
887    
888            boolean errorsLoading = false;
889            LaborOriginEntry laborOriginEntry = null;
890            INPUT_GLE_FILE_br = new BufferedReader(INPUT_GLE_FILE);
891            INPUT_ERR_FILE_br = new BufferedReader(INPUT_ERR_FILE);
892    
893            try {
894                String currentValidLine = INPUT_GLE_FILE_br.readLine();
895                String currentErrorLine = INPUT_ERR_FILE_br.readLine();
896    
897                boolean meetFlag = false;
898                while (currentValidLine != null || currentErrorLine != null) {
899                    // validLine is null means that errorLine is not null
900                    if (StringUtils.isEmpty(currentValidLine)) {
901                        createOutputEntry(currentErrorLine, OUTPUT_DEMERGER_ERR_FILE_ps);
902                        currentErrorLine = INPUT_ERR_FILE_br.readLine();
903                        errorRead++;
904                        errorSaved++;
905    
906                        continue;
907                    }
908    
909                    // errorLine is null means that validLine is not null
910                    if (StringUtils.isEmpty(currentErrorLine)) {
911                        createOutputEntry(currentValidLine, OUTPUT_DEMERGER_GLE_FILE_ps);
912                        currentValidLine = INPUT_GLE_FILE_br.readLine();
913                        validRead++;
914                        validSaved++;
915    
916                        continue;
917                    }
918    
919                    String documentTypeCode = currentErrorLine.substring(pMap.get(KFSPropertyConstants.FINANCIAL_DOCUMENT_TYPE_CODE), pMap.get(KFSPropertyConstants.FINANCIAL_SYSTEM_ORIGINATION_CODE));
920                    if (documentTypeCode != null) {
921                        documentTypeCode = documentTypeCode.trim();
922                    }
923    
924                    if (demergeDocumentTypes.contains(documentTypeCode)) {
925                        String compareStringFromValidEntry = currentValidLine.substring(pMap.get(KFSPropertyConstants.FINANCIAL_DOCUMENT_TYPE_CODE), pMap.get(KFSPropertyConstants.TRANSACTION_ENTRY_SEQUENCE_NUMBER));
926                        String compareStringFromErrorEntry = currentErrorLine.substring(pMap.get(KFSPropertyConstants.FINANCIAL_DOCUMENT_TYPE_CODE), pMap.get(KFSPropertyConstants.TRANSACTION_ENTRY_SEQUENCE_NUMBER));
927    
928                        if (compareStringFromValidEntry.compareTo(compareStringFromErrorEntry) < 0) {
929                            createOutputEntry(currentValidLine, OUTPUT_DEMERGER_GLE_FILE_ps);
930                            currentValidLine = INPUT_GLE_FILE_br.readLine();
931                            validRead++;
932                            validSaved++;
933    
934                        }
935                        else if (compareStringFromValidEntry.compareTo(compareStringFromErrorEntry) > 0) {
936                            createOutputEntry(currentErrorLine, OUTPUT_DEMERGER_ERR_FILE_ps);
937                            currentErrorLine = INPUT_ERR_FILE_br.readLine();
938                            errorRead++;
939                            errorSaved++;
940                        }
941                        else {
942                            createOutputEntry(currentValidLine, OUTPUT_DEMERGER_ERR_FILE_ps);
943                            currentValidLine = INPUT_GLE_FILE_br.readLine();
944                            validRead++;
945                            errorSaved++;
946                        }
947    
948                        continue;
949                    }
950                    createOutputEntry(currentErrorLine, OUTPUT_DEMERGER_ERR_FILE_ps);
951                    currentErrorLine = INPUT_ERR_FILE_br.readLine();
952                    errorRead++;
953                    errorSaved++;
954                }
955    
956            }
957            catch (Exception e) {
958                LOG.error("performDemerger() s" + "topped due to: " + e.getMessage(), e);
959                throw new RuntimeException("performDemerger() stopped due to: " + e.getMessage(), e);
960            }
961            finally {
962                
963                try {
964                    if (INPUT_GLE_FILE_br != null) {
965                        INPUT_GLE_FILE_br.close();
966                    }
967                }
968                catch (IOException e) {
969                    LOG.error("performDemerger() s" + "Failed to close resources due to: " + e.getMessage(), e);
970                }
971                
972                try {
973                    if (INPUT_ERR_FILE_br != null) {
974                        INPUT_ERR_FILE_br.close();
975                    }
976                }
977                catch (IOException e) {
978                    LOG.error("performDemerger() s" + "Failed to close resources due to: " + e.getMessage(), e);
979                }
980    
981                OUTPUT_DEMERGER_GLE_FILE_ps.close();
982                OUTPUT_DEMERGER_ERR_FILE_ps.close();
983            }
984    
985            demergerReport.setValidTransactionsRead(validRead);
986            demergerReport.setValidTransactionsSaved(validSaved);
987            demergerReport.setErrorTransactionsRead(errorRead);
988            demergerReport.setErrorTransactionWritten(errorSaved);
989    
990            this.laborDemergerReportWriterService.writeStatisticLine("SCRUBBER ERROR TRANSACTIONS READ       %,9d", demergerReport.getErrorTransactionsRead());
991            this.laborDemergerReportWriterService.writeStatisticLine("SCRUBBER VALID TRANSACTIONS READ       %,9d", demergerReport.getValidTransactionsRead());
992            this.laborDemergerReportWriterService.writeStatisticLine("DEMERGER ERRORS SAVED                  %,9d", demergerReport.getErrorTransactionsSaved());
993            this.laborDemergerReportWriterService.writeStatisticLine("DEMERGER VALID TRANSACTIONS SAVED      %,9d", demergerReport.getValidTransactionsSaved());
994    
995            this.generateScrubberErrorListingReport(demergerErrorOutputFilename);
996        }
997    
998    
999        protected void createOutputEntry(LaborOriginEntry entry, PrintStream ps) throws IOException {
1000            try {
1001                ps.printf("%s\n", entry.getLine());
1002            }
1003            catch (Exception e) {
1004                throw new IOException(e.toString(),e);
1005            }
1006        }
1007    
1008        protected void createOutputEntry(String line, PrintStream ps) throws IOException {
1009            try {
1010                ps.printf("%s\n", line);
1011            }
1012            catch (Exception e) {
1013                throw new IOException(e.toString(),e);
1014            }
1015        }
1016    
1017        protected boolean checkEntry(LaborOriginEntry validEntry, LaborOriginEntry errorEntry, String documentTypeCode) {
1018            String documentNumber = errorEntry.getDocumentNumber();
1019            String originationCode = errorEntry.getFinancialSystemOriginationCode();
1020    
1021            if (validEntry.getDocumentNumber().equals(documentNumber) && validEntry.getFinancialDocumentTypeCode().equals(documentTypeCode) && validEntry.getFinancialSystemOriginationCode().equals(originationCode)) {
1022                return true;
1023            }
1024            return false;
1025        }
1026    
1027        /**
1028         * Generates a transaction listing report for labor origin entries that were valid
1029         */
1030        protected void generateScrubberTransactionsOnline() {
1031            try {
1032                Iterator<LaborOriginEntry> generatedTransactions = new LaborOriginEntryFileIterator(new File(inputFile));
1033    
1034                ((WrappingBatchService) laborGeneratedTransactionsReportWriterService).initialize();
1035                new TransactionListingReport().generateReport(laborGeneratedTransactionsReportWriterService, generatedTransactions);
1036            }
1037            finally {
1038                ((WrappingBatchService) laborGeneratedTransactionsReportWriterService).destroy();
1039            }
1040        }
1041    
1042        /**
1043         * Generates a transaction listing report for labor origin entries with bad balance types
1044         */
1045        protected void generateScrubberBadBalanceTypeListingReport() {
1046            LaborOriginEntryFilter blankBalanceTypeFilter = new LaborOriginEntryFilter() {
1047                public boolean accept(LaborOriginEntry originEntry) {
1048                    BalanceType originEntryBalanceType = laborAccountingCycleCachingService.getBalanceType(originEntry.getFinancialBalanceTypeCode());
1049                    return ObjectUtils.isNull(originEntryBalanceType);
1050                }
1051            };
1052            try {
1053                ((WrappingBatchService) laborBadBalanceTypeReportWriterService).initialize();
1054                Iterator<LaborOriginEntry> blankBalanceOriginEntries = new FilteringLaborOriginEntryFileIterator(new File(inputFile), blankBalanceTypeFilter);
1055                new TransactionListingReport().generateReport(laborBadBalanceTypeReportWriterService, blankBalanceOriginEntries);
1056            }
1057            finally {
1058                ((WrappingBatchService) laborBadBalanceTypeReportWriterService).destroy();
1059            }
1060        }
1061    
1062        /**
1063         * Generates a transaction listing report for labor origin entries with errors
1064         */
1065        protected void generateScrubberErrorListingReport(String errorFileName) {
1066            try {
1067                ((WrappingBatchService) laborErrorListingReportWriterService).initialize();
1068                Iterator<LaborOriginEntry> removedTransactions = new LaborOriginEntryFileIterator(new File(errorFileName));
1069                new TransactionListingReport().generateReport(laborErrorListingReportWriterService, removedTransactions);
1070            }
1071            finally {
1072                ((WrappingBatchService) laborErrorListingReportWriterService).destroy();
1073            }
1074        }
1075    }