org.kuali.kfs.gl.batch.service.impl.ScrubberProcessImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.kfs.gl.batch.service.impl.ScrubberProcessImpl.java

Source

/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 * 
 * Copyright 2005-2014 The Kuali Foundation
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.gl.batch.service.impl;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.sql.Date;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.LineIterator;
import org.kuali.kfs.coa.businessobject.A21SubAccount;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.BalanceType;
import org.kuali.kfs.coa.businessobject.Chart;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.businessobject.OffsetDefinition;
import org.kuali.kfs.gl.GeneralLedgerConstants;
import org.kuali.kfs.gl.ObjectHelper;
import org.kuali.kfs.gl.batch.BatchSortUtil;
import org.kuali.kfs.gl.batch.CollectorBatch;
import org.kuali.kfs.gl.batch.DemergerSortComparator;
import org.kuali.kfs.gl.batch.ScrubberSortComparator;
import org.kuali.kfs.gl.batch.ScrubberStep;
import org.kuali.kfs.gl.batch.service.AccountingCycleCachingService;
import org.kuali.kfs.gl.batch.service.RunDateService;
import org.kuali.kfs.gl.batch.service.ScrubberProcess;
import org.kuali.kfs.gl.batch.service.impl.FilteringOriginEntryFileIterator.OriginEntryFilter;
import org.kuali.kfs.gl.businessobject.DemergerReportData;
import org.kuali.kfs.gl.businessobject.OriginEntryFieldUtil;
import org.kuali.kfs.gl.businessobject.OriginEntryFull;
import org.kuali.kfs.gl.businessobject.OriginEntryInformation;
import org.kuali.kfs.gl.businessobject.Transaction;
import org.kuali.kfs.gl.report.CollectorReportData;
import org.kuali.kfs.gl.report.LedgerSummaryReport;
import org.kuali.kfs.gl.report.PreScrubberReport;
import org.kuali.kfs.gl.report.PreScrubberReportData;
import org.kuali.kfs.gl.report.TransactionListingReport;
import org.kuali.kfs.gl.service.PreScrubberService;
import org.kuali.kfs.gl.service.ScrubberReportData;
import org.kuali.kfs.gl.service.ScrubberValidator;
import org.kuali.kfs.gl.service.impl.ScrubberStatus;
import org.kuali.kfs.gl.service.impl.StringHelper;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.KFSParameterKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.Message;
import org.kuali.kfs.sys.KFSParameterKeyConstants.GlParameterConstants;
import org.kuali.kfs.sys.batch.service.WrappingBatchService;
import org.kuali.kfs.sys.businessobject.SystemOptions;
import org.kuali.kfs.sys.businessobject.UniversityDate;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.exception.InvalidFlexibleOffsetException;
import org.kuali.kfs.sys.service.DocumentNumberAwareReportWriterService;
import org.kuali.kfs.sys.service.FlexibleOffsetAccountService;
import org.kuali.kfs.sys.service.ReportWriterService;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.parameter.ParameterEvaluator;
import org.kuali.rice.core.api.parameter.ParameterEvaluatorService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.coreservice.framework.parameter.ParameterService;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.service.PersistenceService;
import org.kuali.rice.krad.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * This class has the logic for the scrubber. It is required because the scrubber process needs instance variables. Instance
 * variables in a spring service are shared between all code calling the service. This will make sure each run of the scrubber has
 * it's own instance variables instead of being shared.
 */
public class ScrubberProcessImpl implements ScrubberProcess {
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ScrubberProcessImpl.class);

    protected static final String TRANSACTION_TYPE_COST_SHARE_ENCUMBRANCE = "CE";
    protected static final String TRANSACTION_TYPE_OFFSET = "O";
    protected static final String TRANSACTION_TYPE_CAPITALIZATION = "C";
    protected static final String TRANSACTION_TYPE_LIABILITY = "L";
    protected static final String TRANSACTION_TYPE_TRANSFER = "T";
    protected static final String TRANSACTION_TYPE_COST_SHARE = "CS";
    protected static final String TRANSACTION_TYPE_OTHER = "X";

    enum GROUP_TYPE {
        VALID, ERROR, EXPIRED
    }

    protected static final String COST_SHARE_CODE = "CSHR";

    protected static final String COST_SHARE_TRANSFER_ENTRY_IND = "***";

    // These lengths are different then database field lengths, hence they are not from the DD
    protected static final int COST_SHARE_ENCUMBRANCE_ENTRY_MAXLENGTH = 28;
    protected static final int DEMERGER_TRANSACTION_LEDGET_ENTRY_DESCRIPTION = 33;
    protected static final int OFFSET_MESSAGE_MAXLENGTH = 33;

    /* Services required */
    protected FlexibleOffsetAccountService flexibleOffsetAccountService;
    protected DateTimeService dateTimeService;
    protected ConfigurationService configurationService;
    protected PersistenceService persistenceService;
    protected ScrubberValidator scrubberValidator;
    protected RunDateService runDateService;
    protected AccountingCycleCachingService accountingCycleCachingService;
    protected DocumentNumberAwareReportWriterService scrubberReportWriterService;
    protected DocumentNumberAwareReportWriterService scrubberLedgerReportWriterService;
    protected DocumentNumberAwareReportWriterService scrubberListingReportWriterService;
    protected DocumentNumberAwareReportWriterService preScrubberReportWriterService;
    protected ReportWriterService scrubberBadBalanceListingReportWriterService;
    protected ReportWriterService demergerRemovedTransactionsListingReportWriterService;
    protected ReportWriterService demergerReportWriterService;
    protected PreScrubberService preScrubberService;

    // these three members will only be populated when in collector mode, otherwise the memory requirements will be huge
    protected Map<OriginEntryInformation, OriginEntryInformation> unscrubbedToScrubbedEntries = new HashMap<OriginEntryInformation, OriginEntryInformation>();
    protected Map<Transaction, List<Message>> scrubberReportErrors = new IdentityHashMap<Transaction, List<Message>>();
    protected LedgerSummaryReport ledgerSummaryReport = new LedgerSummaryReport();

    protected ScrubberReportData scrubberReport;
    protected DemergerReportData demergerReport;

    /* These are all different forms of the run date for this job */
    protected Date runDate;
    protected Calendar runCal;
    protected UniversityDate universityRunDate;
    protected String offsetString;

    /* Unit Of Work info */
    protected UnitOfWorkInfo unitOfWork;
    protected KualiDecimal scrubCostShareAmount;

    /* Statistics for the reports */
    protected List<Message> transactionErrors;

    /* Description names */
    protected String offsetDescription;
    protected String capitalizationDescription;
    protected String liabilityDescription;
    protected String transferDescription;
    protected String costShareDescription;

    protected ParameterService parameterService;
    protected BusinessObjectService businessObjectService;

    /**
     * Whether this instance is being used to support the scrubbing of a collector batch
     */
    protected boolean collectorMode;
    protected String batchFileDirectoryName;

    protected PrintStream OUTPUT_GLE_FILE_ps;
    protected PrintStream OUTPUT_ERR_FILE_ps;
    protected PrintStream OUTPUT_EXP_FILE_ps;

    protected String inputFile;
    protected String validFile;
    protected String errorFile;
    protected String expiredFile;

    /**
     * Scrub this single group read only. This will only output the scrubber report. It won't output any other groups.
     *
     * @param group the origin entry group that should be scrubbed
     * @param the document number of any specific entries to scrub
     */
    @Override
    public void scrubGroupReportOnly(String fileName, String documentNumber) {
        LOG.debug("scrubGroupReportOnly() started");
        String unsortedFile = fileName;
        this.inputFile = fileName + ".sort";
        this.validFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.SCRUBBER_VALID_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        this.errorFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.SCRUBBER_ERROR_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        this.expiredFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.SCRUBBER_EXPIRED_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        String prescrubOutput = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.PRE_SCRUBBER_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        this.ledgerSummaryReport = new LedgerSummaryReport();
        runDate = calculateRunDate(dateTimeService.getCurrentDate());

        PreScrubberReportData preScrubberReportData = null;

        // run pre-scrubber on the raw input into the sort process
        LineIterator inputEntries = null;
        try {
            inputEntries = FileUtils.lineIterator(new File(unsortedFile));
            preScrubberReportData = preScrubberService.preprocessOriginEntries(inputEntries, prescrubOutput);
        } catch (IOException e1) {
            LOG.error("Error encountered trying to prescrub GLCP/LLCP document", e1);
            throw new RuntimeException("Error encountered trying to prescrub GLCP/LLCP document", e1);
        } finally {
            LineIterator.closeQuietly(inputEntries);
        }
        if (preScrubberReportData != null) {
            preScrubberReportWriterService.setDocumentNumber(documentNumber);
            ((WrappingBatchService) preScrubberReportWriterService).initialize();
            try {
                new PreScrubberReport().generateReport(preScrubberReportData, preScrubberReportWriterService);
            } finally {
                ((WrappingBatchService) preScrubberReportWriterService).destroy();
            }
        }
        BatchSortUtil.sortTextFileWithFields(prescrubOutput, inputFile, new ScrubberSortComparator());

        scrubEntries(true, documentNumber);

        // delete files
        File deleteSortFile = new File(inputFile);
        File deleteValidFile = new File(validFile);
        File deleteErrorFile = new File(errorFile);
        File deleteExpiredFile = new File(expiredFile);
        try {
            deleteSortFile.delete();
            deleteValidFile.delete();
            deleteErrorFile.delete();
            deleteExpiredFile.delete();
        } catch (Exception e) {
            LOG.error("scrubGroupReportOnly delete output files process Stopped: " + e.getMessage());
            throw new RuntimeException(
                    "scrubGroupReportOnly delete output files process Stopped: " + e.getMessage(), e);
        }
    }

    /**
     * Scrubs all entries in all groups and documents.
     */
    @Override
    public void scrubEntries() {
        this.inputFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.SCRUBBER_INPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        this.validFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.SCRUBBER_VALID_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        this.errorFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.SCRUBBER_ERROR_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        this.expiredFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.SCRUBBER_EXPIRED_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        runDate = calculateRunDate(dateTimeService.getCurrentDate());

        scrubEntries(false, null);
    }

    /**
     * Scrubs the origin entry and ID billing details if the given batch. Store all scrubber output into the collectorReportData
     * parameter. NOTE: DO NOT CALL ANY OF THE scrub* METHODS OF THIS CLASS AFTER CALLING THIS METHOD FOR EVERY UNIQUE INSTANCE OF
     * THIS CLASS, OR THE COLLECTOR REPORTS MAY BE CORRUPTED
     *
     * @param batch the data gathered from a Collector file
     * @param collectorReportData the statistics generated by running the Collector
     */
    @Override
    public void scrubCollectorBatch(ScrubberStatus scrubberStatus, CollectorBatch batch,
            CollectorReportData collectorReportData) {
        collectorMode = true;

        this.inputFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_INPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        this.validFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_VALID_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        this.errorFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_ERROR_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        this.expiredFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_EXPIRED_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        runDate = calculateRunDate(dateTimeService.getCurrentDate());

        this.ledgerSummaryReport = collectorReportData.getLedgerSummaryReport();

        // sort input file
        String scrubberSortInputFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_BACKUP_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        String scrubberSortOutputFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_INPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        BatchSortUtil.sortTextFileWithFields(scrubberSortInputFile, scrubberSortOutputFile,
                new ScrubberSortComparator());

        scrubEntries(false, null);

        //sort scrubber error file for demerger
        String demergerSortInputFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_ERROR_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        String demergerSortOutputFile = batchFileDirectoryName + File.separator
                + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_ERROR_SORTED_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        BatchSortUtil.sortTextFileWithFields(demergerSortInputFile, demergerSortOutputFile,
                new DemergerSortComparator());

        performDemerger();

        // the scrubber process has just updated several member variables of this class. Store these values for the collector report
        collectorReportData.setBatchOriginEntryScrubberErrors(batch, scrubberReportErrors);
        collectorReportData.setScrubberReportData(batch, scrubberReport);
        collectorReportData.setDemergerReportData(batch, demergerReport);

        // report purpose - commented out.  If we need, the put string values for fileNames.
        scrubberStatus.setInputFileName(GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_INPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
        scrubberStatus.setValidFileName(GeneralLedgerConstants.BatchFileSystem.COLLECTOR_DEMERGER_VAILD_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
        scrubberStatus.setErrorFileName(GeneralLedgerConstants.BatchFileSystem.COLLECTOR_DEMERGER_ERROR_OUTPUT_FILE
                + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
        scrubberStatus
                .setExpiredFileName(GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_EXPIRED_OUTPUT_FILE
                        + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
        scrubberStatus.setUnscrubbedToScrubbedEntries(unscrubbedToScrubbedEntries);
    }

    /**
     * Scrub all entries that need it in origin entry. Put valid scrubbed entries in a scrubber valid group, put errors in a
     * scrubber error group, and transactions with an expired account in the scrubber expired account group.
     * @param group the specific origin entry group to scrub
     * @param documentNumber the number of the document with entries to scrub
     */
    @Override
    public void scrubEntries(boolean reportOnlyMode, String documentNumber) {
        LOG.debug("scrubEntries() started");

        if (reportOnlyMode) {
            scrubberReportWriterService.setDocumentNumber(documentNumber);
            scrubberLedgerReportWriterService.setDocumentNumber(documentNumber);
        }

        // setup an object to hold the "default" date information
        runDate = calculateRunDate(dateTimeService.getCurrentDate());
        runCal = Calendar.getInstance();
        runCal.setTime(runDate);

        universityRunDate = accountingCycleCachingService.getUniversityDate(runDate);
        if (universityRunDate == null) {
            throw new IllegalStateException(
                    configurationService.getPropertyValueAsString(KFSKeyConstants.ERROR_UNIV_DATE_NOT_FOUND));
        }

        setOffsetString();
        setDescriptions();
        scrubberReport = new ScrubberReportData();

        try {
            if (!collectorMode) {
                ((WrappingBatchService) scrubberReportWriterService).initialize();
                ((WrappingBatchService) scrubberLedgerReportWriterService).initialize();
            }

            processGroup(reportOnlyMode, scrubberReport);

            /* temp log for debugging output file delay issue
            if (new File(validFile).canRead()) {
            LOG.info("Valid file " + validFile + " is ready for read.");
            }
            else {
            LOG.error("Valid file " + validFile + " is not readable.");
            }
            if (new File(errorFile).canRead()) {
            LOG.info("Error file " + errorFile + " is ready for read.");
            }
            else {
            LOG.error("Error file " + errorFile + " is not readable.");
            }
            if (new File(expiredFile).canRead()) {
            LOG.info("Expired file " + expiredFile + " is ready for read.");
            }
            else {
            LOG.error("Expired file " + expiredFile + " is not readable.");
            }
            */

            if (reportOnlyMode) {
                generateScrubberTransactionListingReport(documentNumber, inputFile);
            } else if (collectorMode) {
                // defer report generation for later
            } else {
                generateScrubberBlankBalanceTypeCodeReport(inputFile);
            }
        } finally {
            if (!collectorMode) {
                ((WrappingBatchService) scrubberReportWriterService).destroy();
                ((WrappingBatchService) scrubberLedgerReportWriterService).destroy();
            }
        }
    }

    /**
     * The demerger process reads all of the documents in the error group, then moves all of the original entries for that document
     * from the valid group to the error group. It does not move generated entries to the error group. Those are deleted. It also
     * modifies the doc number and origin code of cost share transfers.
     *
     * @param errorGroup this scrubber run's error group
     * @param validGroup this scrubber run's valid group
     */
    @Override
    public void performDemerger() {
        LOG.debug("performDemerger() started");

        OriginEntryFieldUtil oefu = new OriginEntryFieldUtil();
        Map<String, Integer> pMap = oefu.getFieldBeginningPositionMap();

        // Without this step, the job fails with Optimistic Lock Exceptions
        //   persistenceService.clearCache();

        demergerReport = new DemergerReportData();

        // set runDate here again, because demerger is calling outside from scrubber
        runDate = calculateRunDate(dateTimeService.getCurrentDate());
        runCal = Calendar.getInstance();
        runCal.setTime(runDate);

        // demerger called by outside from scrubber, so reset those values
        setOffsetString();
        setDescriptions();

        // new demerger starts

        String validOutputFilename = null;
        String errorOutputFilename = null;

        String demergerValidOutputFilename = null;
        String demergerErrorOutputFilename = null;

        if (!collectorMode) {
            validOutputFilename = batchFileDirectoryName + File.separator
                    + GeneralLedgerConstants.BatchFileSystem.SCRUBBER_VALID_OUTPUT_FILE
                    + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
            errorOutputFilename = batchFileDirectoryName + File.separator
                    + GeneralLedgerConstants.BatchFileSystem.SCRUBBER_ERROR_SORTED_FILE
                    + GeneralLedgerConstants.BatchFileSystem.EXTENSION;

            demergerValidOutputFilename = batchFileDirectoryName + File.separator
                    + GeneralLedgerConstants.BatchFileSystem.DEMERGER_VAILD_OUTPUT_FILE
                    + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
            demergerErrorOutputFilename = batchFileDirectoryName + File.separator
                    + GeneralLedgerConstants.BatchFileSystem.DEMERGER_ERROR_OUTPUT_FILE
                    + GeneralLedgerConstants.BatchFileSystem.EXTENSION;

        } else {

            validOutputFilename = batchFileDirectoryName + File.separator
                    + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_VALID_OUTPUT_FILE
                    + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
            errorOutputFilename = batchFileDirectoryName + File.separator
                    + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_SCRUBBER_ERROR_SORTED_FILE
                    + GeneralLedgerConstants.BatchFileSystem.EXTENSION;

            demergerValidOutputFilename = batchFileDirectoryName + File.separator
                    + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_DEMERGER_VAILD_OUTPUT_FILE
                    + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
            demergerErrorOutputFilename = batchFileDirectoryName + File.separator
                    + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_DEMERGER_ERROR_OUTPUT_FILE
                    + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
        }

        // Without this step, the job fails with Optimistic Lock Exceptions
        //    persistenceService.clearCache();

        FileReader INPUT_GLE_FILE = null;
        FileReader INPUT_ERR_FILE = null;
        BufferedReader INPUT_GLE_FILE_br;
        BufferedReader INPUT_ERR_FILE_br;
        PrintStream OUTPUT_DEMERGER_GLE_FILE_ps;
        PrintStream OUTPUT_DEMERGER_ERR_FILE_ps;

        try {
            INPUT_GLE_FILE = new FileReader(validOutputFilename);
            INPUT_ERR_FILE = new FileReader(errorOutputFilename);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        try {
            OUTPUT_DEMERGER_GLE_FILE_ps = new PrintStream(demergerValidOutputFilename);
            OUTPUT_DEMERGER_ERR_FILE_ps = new PrintStream(demergerErrorOutputFilename);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        int validSaved = 0;
        int errorSaved = 0;

        int validReadLine = 0;
        int errorReadLine = 0;

        boolean errorsLoading = false;
        INPUT_GLE_FILE_br = new BufferedReader(INPUT_GLE_FILE);
        INPUT_ERR_FILE_br = new BufferedReader(INPUT_ERR_FILE);

        try {
            String currentValidLine = INPUT_GLE_FILE_br.readLine();
            String currentErrorLine = INPUT_ERR_FILE_br.readLine();

            boolean meetFlag = false;

            while (currentValidLine != null || currentErrorLine != null) {

                // Demerger only catch IOexception since demerger report doesn't display
                // detail error message.
                try {
                    //validLine is null means that errorLine is not null
                    if (org.apache.commons.lang.StringUtils.isEmpty(currentValidLine)) {
                        String errorDesc = currentErrorLine.substring(
                                pMap.get(KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_DESC),
                                pMap.get(KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_AMOUNT));
                        String errorFinancialBalanceTypeCode = currentErrorLine.substring(
                                pMap.get(KFSPropertyConstants.FINANCIAL_BALANCE_TYPE_CODE),
                                pMap.get(KFSPropertyConstants.FINANCIAL_OBJECT_TYPE_CODE));

                        if (!checkingBypassEntry(errorFinancialBalanceTypeCode, errorDesc, demergerReport)) {
                            createOutputEntry(currentErrorLine, OUTPUT_DEMERGER_ERR_FILE_ps);
                            errorSaved++;
                        }
                        currentErrorLine = INPUT_ERR_FILE_br.readLine();
                        errorReadLine++;
                        continue;
                    }

                    String financialBalanceTypeCode = currentValidLine.substring(
                            pMap.get(KFSPropertyConstants.FINANCIAL_BALANCE_TYPE_CODE),
                            pMap.get(KFSPropertyConstants.FINANCIAL_OBJECT_TYPE_CODE));
                    String desc = currentValidLine.substring(
                            pMap.get(KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_DESC),
                            pMap.get(KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_AMOUNT));

                    //errorLine is null means that validLine is not null
                    if (org.apache.commons.lang.StringUtils.isEmpty(currentErrorLine)) {
                        // Read all the transactions in the valid group and update the cost share transactions
                        String updatedValidLine = checkAndSetTransactionTypeCostShare(financialBalanceTypeCode,
                                desc, currentValidLine);
                        createOutputEntry(updatedValidLine, OUTPUT_DEMERGER_GLE_FILE_ps);
                        handleDemergerSaveValidEntry(updatedValidLine);
                        validSaved++;
                        currentValidLine = INPUT_GLE_FILE_br.readLine();
                        validReadLine++;
                        continue;
                    }

                    String compareStringFromValidEntry = currentValidLine.substring(
                            pMap.get(KFSPropertyConstants.FINANCIAL_DOCUMENT_TYPE_CODE),
                            pMap.get(KFSPropertyConstants.TRANSACTION_ENTRY_SEQUENCE_NUMBER));
                    String compareStringFromErrorEntry = currentErrorLine.substring(
                            pMap.get(KFSPropertyConstants.FINANCIAL_DOCUMENT_TYPE_CODE),
                            pMap.get(KFSPropertyConstants.TRANSACTION_ENTRY_SEQUENCE_NUMBER));

                    String errorDesc = currentErrorLine.substring(
                            pMap.get(KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_DESC),
                            pMap.get(KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_AMOUNT));
                    String errorFinancialBalanceTypeCode = currentErrorLine.substring(
                            pMap.get(KFSPropertyConstants.FINANCIAL_BALANCE_TYPE_CODE),
                            pMap.get(KFSPropertyConstants.FINANCIAL_OBJECT_TYPE_CODE));

                    if (compareStringFromValidEntry.compareTo(compareStringFromErrorEntry) < 0) {
                        // Read all the transactions in the valid group and update the cost share transactions
                        String updatedValidLine = checkAndSetTransactionTypeCostShare(financialBalanceTypeCode,
                                desc, currentValidLine);
                        createOutputEntry(updatedValidLine, OUTPUT_DEMERGER_GLE_FILE_ps);
                        handleDemergerSaveValidEntry(updatedValidLine);
                        validSaved++;
                        currentValidLine = INPUT_GLE_FILE_br.readLine();
                        validReadLine++;

                    } else if (compareStringFromValidEntry.compareTo(compareStringFromErrorEntry) > 0) {
                        if (!checkingBypassEntry(errorFinancialBalanceTypeCode, errorDesc, demergerReport)) {
                            createOutputEntry(currentErrorLine, OUTPUT_DEMERGER_ERR_FILE_ps);
                            errorSaved++;
                        }
                        currentErrorLine = INPUT_ERR_FILE_br.readLine();
                        errorReadLine++;

                    } else {
                        if (!checkingBypassEntry(financialBalanceTypeCode, desc, demergerReport)) {
                            createOutputEntry(currentValidLine, OUTPUT_DEMERGER_ERR_FILE_ps);
                            errorSaved++;
                        }
                        currentValidLine = INPUT_GLE_FILE_br.readLine();
                        validReadLine++;
                    }

                    continue;

                } catch (RuntimeException re) {
                    LOG.error("performDemerger Stopped: " + re.getMessage());
                    throw new RuntimeException("performDemerger Stopped: " + re.getMessage(), re);
                }
            }
            INPUT_GLE_FILE_br.close();
            INPUT_ERR_FILE_br.close();
            OUTPUT_DEMERGER_GLE_FILE_ps.close();
            OUTPUT_DEMERGER_ERR_FILE_ps.close();

        } catch (IOException e) {
            LOG.error("performDemerger Stopped: " + e.getMessage());
            throw new RuntimeException("performDemerger Stopped: " + e.getMessage(), e);
        }
        demergerReport.setErrorTransactionWritten(errorSaved);
        demergerReport.setErrorTransactionsRead(errorReadLine);
        demergerReport.setValidTransactionsRead(validReadLine);
        demergerReport.setValidTransactionsSaved(validSaved);

        if (!collectorMode) {
            demergerReportWriterService.writeStatisticLine("SCRUBBER ERROR TRANSACTIONS READ       %,9d",
                    demergerReport.getErrorTransactionsRead());
            demergerReportWriterService.writeStatisticLine("SCRUBBER VALID TRANSACTIONS READ       %,9d",
                    demergerReport.getValidTransactionsRead());
            demergerReportWriterService.writeNewLines(1);
            demergerReportWriterService.writeStatisticLine("DEMERGER ERRORS SAVED                  %,9d",
                    demergerReport.getErrorTransactionsSaved());
            demergerReportWriterService.writeStatisticLine("DEMERGER VALID TRANSACTIONS SAVED      %,9d",
                    demergerReport.getValidTransactionsSaved());
            demergerReportWriterService.writeStatisticLine("OFFSET TRANSACTIONS BYPASSED           %,9d",
                    demergerReport.getOffsetTransactionsBypassed());
            demergerReportWriterService.writeStatisticLine("CAPITALIZATION TRANSACTIONS BYPASSED   %,9d",
                    demergerReport.getCapitalizationTransactionsBypassed());
            demergerReportWriterService.writeStatisticLine("LIABILITY TRANSACTIONS BYPASSED        %,9d",
                    demergerReport.getLiabilityTransactionsBypassed());
            demergerReportWriterService.writeStatisticLine("TRANSFER TRANSACTIONS BYPASSED         %,9d",
                    demergerReport.getTransferTransactionsBypassed());
            demergerReportWriterService.writeStatisticLine("COST SHARE TRANSACTIONS BYPASSED       %,9d",
                    demergerReport.getCostShareTransactionsBypassed());
            demergerReportWriterService.writeStatisticLine("COST SHARE ENC TRANSACTIONS BYPASSED   %,9d",
                    demergerReport.getCostShareEncumbranceTransactionsBypassed());

            generateDemergerRemovedTransactionsReport(demergerErrorOutputFilename);
        }
    }

    /**
     * Determine the type of the transaction by looking at attributes
     *
     * @param transaction Transaction to identify
     * @return CE (Cost share encumbrance, O (Offset), C (apitalization), L (Liability), T (Transfer), CS (Cost Share), X (Other)
     */
    protected String getTransactionType(OriginEntryInformation transaction) {
        if (TRANSACTION_TYPE_COST_SHARE_ENCUMBRANCE.equals(transaction.getFinancialBalanceTypeCode())) {
            return TRANSACTION_TYPE_COST_SHARE_ENCUMBRANCE;
        }
        String desc = transaction.getTransactionLedgerEntryDescription();

        if (desc == null) {
            return TRANSACTION_TYPE_OTHER;
        }
        if (desc.startsWith(offsetDescription) && desc.indexOf(COST_SHARE_TRANSFER_ENTRY_IND) > -1) {
            return TRANSACTION_TYPE_COST_SHARE;
        }
        if (desc.startsWith(costShareDescription) && desc.indexOf(COST_SHARE_TRANSFER_ENTRY_IND) > -1) {
            return TRANSACTION_TYPE_COST_SHARE;
        }
        if (desc.startsWith(offsetDescription)) {
            return TRANSACTION_TYPE_OFFSET;
        }
        if (desc.startsWith(capitalizationDescription)) {
            return TRANSACTION_TYPE_CAPITALIZATION;
        }
        if (desc.startsWith(liabilityDescription)) {
            return TRANSACTION_TYPE_LIABILITY;
        }
        if (desc.startsWith(transferDescription)) {
            return TRANSACTION_TYPE_TRANSFER;
        }
        return TRANSACTION_TYPE_OTHER;
    }

    protected String getTransactionType(String financialBalanceTypeCode, String desc) {
        if (TRANSACTION_TYPE_COST_SHARE_ENCUMBRANCE.equals(financialBalanceTypeCode)) {
            return TRANSACTION_TYPE_COST_SHARE_ENCUMBRANCE;
        }
        if (desc == null) {
            return TRANSACTION_TYPE_OTHER;
        }

        if (desc.startsWith(offsetDescription) && desc.indexOf(COST_SHARE_TRANSFER_ENTRY_IND) > -1) {
            return TRANSACTION_TYPE_COST_SHARE;
        }
        if (desc.startsWith(costShareDescription) && desc.indexOf(COST_SHARE_TRANSFER_ENTRY_IND) > -1) {
            return TRANSACTION_TYPE_COST_SHARE;
        }
        if (desc.startsWith(offsetDescription)) {
            return TRANSACTION_TYPE_OFFSET;
        }
        if (desc.startsWith(capitalizationDescription)) {
            return TRANSACTION_TYPE_CAPITALIZATION;
        }
        if (desc.startsWith(liabilityDescription)) {
            return TRANSACTION_TYPE_LIABILITY;
        }
        if (desc.startsWith(transferDescription)) {
            return TRANSACTION_TYPE_TRANSFER;
        }
        return TRANSACTION_TYPE_OTHER;
    }

    /**
     * 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
     * of Cobol relating to this.
     *
     * @param originEntryGroup Group to process
     */
    protected void processGroup(boolean reportOnlyMode, ScrubberReportData scrubberReport) {
        OriginEntryFull lastEntry = null;
        scrubCostShareAmount = KualiDecimal.ZERO;
        unitOfWork = new UnitOfWorkInfo();

        FileReader INPUT_GLE_FILE = null;
        String GLEN_RECORD;
        BufferedReader INPUT_GLE_FILE_br;
        try {
            INPUT_GLE_FILE = new FileReader(inputFile);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        try {
            OUTPUT_GLE_FILE_ps = new PrintStream(validFile);
            OUTPUT_ERR_FILE_ps = new PrintStream(errorFile);
            OUTPUT_EXP_FILE_ps = new PrintStream(expiredFile);
            LOG.info("Successfully opened " + validFile + ", " + errorFile + ", " + expiredFile + " for writing.");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        INPUT_GLE_FILE_br = new BufferedReader(INPUT_GLE_FILE);
        int line = 0;
        LOG.debug("Starting Scrubber Process process group...");
        try {
            while ((GLEN_RECORD = INPUT_GLE_FILE_br.readLine()) != null) {
                if (!org.apache.commons.lang.StringUtils.isEmpty(GLEN_RECORD)
                        && !org.apache.commons.lang.StringUtils.isBlank(GLEN_RECORD.trim())) {
                    line++;
                    OriginEntryFull unscrubbedEntry = new OriginEntryFull();
                    List<Message> tmperrors = unscrubbedEntry.setFromTextFileForBatch(GLEN_RECORD, line);
                    scrubberReport.incrementUnscrubbedRecordsRead();
                    transactionErrors = new ArrayList<Message>();

                    //
                    // This is done so if the code modifies this row, then saves it, it will be an insert,
                    // and it won't touch the original. The Scrubber never modifies input rows/groups.
                    // not relevant for file version

                    boolean saveErrorTransaction = false;
                    boolean saveValidTransaction = false;
                    boolean fatalErrorOccurred = false;

                    // Build a scrubbed entry
                    OriginEntryFull scrubbedEntry = new OriginEntryFull();
                    scrubbedEntry.setDocumentNumber(unscrubbedEntry.getDocumentNumber());
                    scrubbedEntry.setOrganizationDocumentNumber(unscrubbedEntry.getOrganizationDocumentNumber());
                    scrubbedEntry.setOrganizationReferenceId(unscrubbedEntry.getOrganizationReferenceId());
                    scrubbedEntry.setReferenceFinancialDocumentNumber(
                            unscrubbedEntry.getReferenceFinancialDocumentNumber());

                    Integer transactionNumber = unscrubbedEntry.getTransactionLedgerEntrySequenceNumber();
                    scrubbedEntry.setTransactionLedgerEntrySequenceNumber(
                            null == transactionNumber ? new Integer(0) : transactionNumber);
                    scrubbedEntry.setTransactionLedgerEntryDescription(
                            unscrubbedEntry.getTransactionLedgerEntryDescription());
                    scrubbedEntry
                            .setTransactionLedgerEntryAmount(unscrubbedEntry.getTransactionLedgerEntryAmount());
                    scrubbedEntry.setTransactionDebitCreditCode(unscrubbedEntry.getTransactionDebitCreditCode());

                    if (!collectorMode) {
                        ledgerSummaryReport.summarizeEntry(unscrubbedEntry);
                    }

                    // For Labor Scrubber
                    boolean laborIndicator = false;
                    tmperrors.addAll(scrubberValidator.validateTransaction(unscrubbedEntry, scrubbedEntry,
                            universityRunDate, laborIndicator, accountingCycleCachingService));
                    transactionErrors.addAll(tmperrors);

                    Account unscrubbedEntryAccount = accountingCycleCachingService.getAccount(
                            unscrubbedEntry.getChartOfAccountsCode(), unscrubbedEntry.getAccountNumber());
                    // KFSMI-173: both the expired and closed accounts rows are put in the expired account
                    if ((unscrubbedEntryAccount != null)
                            && (scrubberValidator.isAccountExpired(unscrubbedEntryAccount, universityRunDate)
                                    || unscrubbedEntryAccount.isClosed())) {
                        // Make a copy of it so OJB doesn't just update the row in the original
                        // group. It needs to make a new one in the expired group
                        OriginEntryFull expiredEntry = OriginEntryFull.copyFromOriginEntryable(scrubbedEntry);
                        createOutputEntry(expiredEntry, OUTPUT_EXP_FILE_ps);
                        scrubberReport.incrementExpiredAccountFound();
                    }

                    // the collector scrubber uses this map to apply the same changes made on an origin entry during scrubbing to
                    // the collector detail record
                    if (collectorMode) {
                        unscrubbedToScrubbedEntries.put(unscrubbedEntry, scrubbedEntry);
                    }

                    if (!isFatal(transactionErrors)) {
                        saveValidTransaction = true;

                        if (!collectorMode) {

                            // See if unit of work has changed
                            if (!unitOfWork.isSameUnitOfWork(scrubbedEntry)) {
                                // Generate offset for last unit of work
                                // pass the String line for generating error files
                                generateOffset(lastEntry, scrubberReport);

                                unitOfWork = new UnitOfWorkInfo(scrubbedEntry);
                            }

                            KualiDecimal transactionAmount = scrubbedEntry.getTransactionLedgerEntryAmount();

                            ParameterEvaluator offsetFiscalPeriods = /*REFACTORME*/SpringContext
                                    .getBean(ParameterEvaluatorService.class)
                                    .getParameterEvaluator(ScrubberStep.class,
                                            GeneralLedgerConstants.GlScrubberGroupRules.OFFSET_FISCAL_PERIOD_CODES,
                                            scrubbedEntry.getUniversityFiscalPeriodCode());

                            BalanceType scrubbedEntryBalanceType = accountingCycleCachingService
                                    .getBalanceType(scrubbedEntry.getFinancialBalanceTypeCode());
                            if (scrubbedEntryBalanceType.isFinancialOffsetGenerationIndicator()
                                    && offsetFiscalPeriods.evaluationSucceeds()) {
                                if (scrubbedEntry.isDebit()) {
                                    unitOfWork.offsetAmount = unitOfWork.offsetAmount.add(transactionAmount);
                                } else {
                                    unitOfWork.offsetAmount = unitOfWork.offsetAmount.subtract(transactionAmount);
                                }
                            }

                            // The sub account type code will only exist if there is a valid sub account
                            String subAccountTypeCode = GeneralLedgerConstants.getSpaceSubAccountTypeCode();
                            // major assumption: the a21 subaccount is proxied, so we don't want to query the database if the
                            // subacct
                            // number is dashes
                            if (!KFSConstants.getDashSubAccountNumber()
                                    .equals(scrubbedEntry.getSubAccountNumber())) {
                                A21SubAccount scrubbedEntryA21SubAccount = accountingCycleCachingService
                                        .getA21SubAccount(scrubbedEntry.getChartOfAccountsCode(),
                                                scrubbedEntry.getAccountNumber(),
                                                scrubbedEntry.getSubAccountNumber());
                                if (ObjectUtils.isNotNull(scrubbedEntryA21SubAccount)) {
                                    subAccountTypeCode = scrubbedEntryA21SubAccount.getSubAccountTypeCode();
                                }
                            }

                            ParameterEvaluator costShareObjectTypeCodes = /*REFACTORME*/SpringContext
                                    .getBean(ParameterEvaluatorService.class)
                                    .getParameterEvaluator(ScrubberStep.class,
                                            GeneralLedgerConstants.GlScrubberGroupRules.COST_SHARE_OBJ_TYPE_CODES,
                                            scrubbedEntry.getFinancialObjectTypeCode());
                            ParameterEvaluator costShareEncBalanceTypeCodes = /*REFACTORME*/SpringContext
                                    .getBean(ParameterEvaluatorService.class)
                                    .getParameterEvaluator(ScrubberStep.class,
                                            GeneralLedgerConstants.GlScrubberGroupRules.COST_SHARE_ENC_BAL_TYP_CODES,
                                            scrubbedEntry.getFinancialBalanceTypeCode());
                            ParameterEvaluator costShareEncFiscalPeriodCodes = /*REFACTORME*/SpringContext
                                    .getBean(ParameterEvaluatorService.class)
                                    .getParameterEvaluator(ScrubberStep.class,
                                            GeneralLedgerConstants.GlScrubberGroupRules.COST_SHARE_ENC_FISCAL_PERIOD_CODES,
                                            scrubbedEntry.getUniversityFiscalPeriodCode());
                            ParameterEvaluator costShareEncDocTypeCodes = /*REFACTORME*/SpringContext
                                    .getBean(ParameterEvaluatorService.class)
                                    .getParameterEvaluator(ScrubberStep.class,
                                            GeneralLedgerConstants.GlScrubberGroupRules.COST_SHARE_ENC_DOC_TYPE_CODES,
                                            scrubbedEntry.getFinancialDocumentTypeCode().trim());
                            ParameterEvaluator costShareFiscalPeriodCodes = /*REFACTORME*/SpringContext
                                    .getBean(ParameterEvaluatorService.class)
                                    .getParameterEvaluator(ScrubberStep.class,
                                            GeneralLedgerConstants.GlScrubberGroupRules.COST_SHARE_FISCAL_PERIOD_CODES,
                                            scrubbedEntry.getUniversityFiscalPeriodCode());
                            Account scrubbedEntryAccount = accountingCycleCachingService.getAccount(
                                    scrubbedEntry.getChartOfAccountsCode(), scrubbedEntry.getAccountNumber());

                            if (costShareObjectTypeCodes.evaluationSucceeds()
                                    && costShareEncBalanceTypeCodes.evaluationSucceeds()
                                    && scrubbedEntryAccount.isForContractsAndGrants()
                                    && KFSConstants.SubAccountType.COST_SHARE.equals(subAccountTypeCode)
                                    && costShareEncFiscalPeriodCodes.evaluationSucceeds()
                                    && costShareEncDocTypeCodes.evaluationSucceeds()) {
                                TransactionError te1 = generateCostShareEncumbranceEntries(scrubbedEntry,
                                        scrubberReport);
                                if (te1 != null) {
                                    List errors = new ArrayList();
                                    errors.add(te1.message);
                                    handleTransactionErrors(te1.transaction, errors);
                                    saveValidTransaction = false;
                                    saveErrorTransaction = true;
                                }
                            }

                            SystemOptions scrubbedEntryOption = accountingCycleCachingService
                                    .getSystemOptions(scrubbedEntry.getUniversityFiscalYear());
                            if (costShareObjectTypeCodes.evaluationSucceeds()
                                    && scrubbedEntryOption.getActualFinancialBalanceTypeCd()
                                            .equals(scrubbedEntry.getFinancialBalanceTypeCode())
                                    && scrubbedEntryAccount.isForContractsAndGrants()
                                    && KFSConstants.SubAccountType.COST_SHARE.equals(subAccountTypeCode)
                                    && costShareFiscalPeriodCodes.evaluationSucceeds()
                                    && costShareEncDocTypeCodes.evaluationSucceeds()) {
                                if (scrubbedEntry.isDebit()) {
                                    scrubCostShareAmount = scrubCostShareAmount.subtract(transactionAmount);
                                } else {
                                    scrubCostShareAmount = scrubCostShareAmount.add(transactionAmount);
                                }
                            }

                            ParameterEvaluator otherDocTypeCodes = /*REFACTORME*/SpringContext
                                    .getBean(ParameterEvaluatorService.class)
                                    .getParameterEvaluator(ScrubberStep.class,
                                            GeneralLedgerConstants.GlScrubberGroupRules.OFFSET_DOC_TYPE_CODES,
                                            scrubbedEntry.getFinancialDocumentTypeCode());

                            if (otherDocTypeCodes.evaluationSucceeds()) {
                                String m = processCapitalization(scrubbedEntry, scrubberReport);
                                if (m != null) {
                                    saveValidTransaction = false;
                                    saveErrorTransaction = false;
                                    addTransactionError(m, "", Message.TYPE_FATAL);
                                }

                                m = processLiabilities(scrubbedEntry, scrubberReport);
                                if (m != null) {
                                    saveValidTransaction = false;
                                    saveErrorTransaction = false;
                                    addTransactionError(m, "", Message.TYPE_FATAL);
                                }

                                m = processPlantIndebtedness(scrubbedEntry, scrubberReport);
                                if (m != null) {
                                    saveValidTransaction = false;
                                    saveErrorTransaction = false;
                                    addTransactionError(m, "", Message.TYPE_FATAL);
                                }
                            }

                            if (!scrubCostShareAmount.isZero()) {
                                TransactionError te = generateCostShareEntries(scrubbedEntry, scrubberReport);

                                if (te != null) {
                                    saveValidTransaction = false;
                                    saveErrorTransaction = false;

                                    // Make a copy of it so OJB doesn't just update the row in the original
                                    // group. It needs to make a new one in the error group
                                    OriginEntryFull errorEntry = new OriginEntryFull(te.transaction);
                                    errorEntry.setTransactionScrubberOffsetGenerationIndicator(false);
                                    createOutputEntry(GLEN_RECORD, OUTPUT_ERR_FILE_ps);
                                    scrubberReport.incrementErrorRecordWritten();
                                    unitOfWork.errorsFound = true;

                                    handleTransactionError(te.transaction, te.message);
                                }
                                scrubCostShareAmount = KualiDecimal.ZERO;
                            }

                            lastEntry = scrubbedEntry;
                        }
                    } else {
                        // Error transaction
                        saveErrorTransaction = true;
                        fatalErrorOccurred = true;
                    }
                    handleTransactionErrors(OriginEntryFull.copyFromOriginEntryable(unscrubbedEntry),
                            transactionErrors);

                    if (saveValidTransaction) {
                        scrubbedEntry.setTransactionScrubberOffsetGenerationIndicator(false);
                        createOutputEntry(scrubbedEntry, OUTPUT_GLE_FILE_ps);
                        scrubberReport.incrementScrubbedRecordWritten();
                    }

                    if (saveErrorTransaction) {
                        // Make a copy of it so OJB doesn't just update the row in the original
                        // group. It needs to make a new one in the error group
                        OriginEntryFull errorEntry = OriginEntryFull.copyFromOriginEntryable(unscrubbedEntry);
                        errorEntry.setTransactionScrubberOffsetGenerationIndicator(false);
                        createOutputEntry(GLEN_RECORD, OUTPUT_ERR_FILE_ps);
                        scrubberReport.incrementErrorRecordWritten();
                        if (!fatalErrorOccurred) {
                            // if a fatal error occurred, the creation of a new unit of work was by-passed;
                            // therefore, it shouldn't ruin the previous unit of work
                            unitOfWork.errorsFound = true;
                        }
                    }
                }
            }

            if (!collectorMode) {
                // Generate last offset (if necessary)
                generateOffset(lastEntry, scrubberReport);
            }

            INPUT_GLE_FILE_br.close();
            INPUT_GLE_FILE.close();
            OUTPUT_GLE_FILE_ps.close();
            OUTPUT_ERR_FILE_ps.close();
            OUTPUT_EXP_FILE_ps.close();
            LOG.info("Successfully writen and closed " + validFile + ", " + errorFile + ", " + expiredFile + ".");

            handleEndOfScrubberReport(scrubberReport);

            if (!collectorMode) {
                ledgerSummaryReport.writeReport(this.scrubberLedgerReportWriterService);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Determines if a given error is fatal and should stop this scrubber run
     *
     * @param errors errors from a scrubber run
     * @return true if the run should be abended, false otherwise
     */
    protected boolean isFatal(List<Message> errors) {
        for (Iterator<Message> iter = errors.iterator(); iter.hasNext();) {
            Message element = iter.next();
            if (element.getType() == Message.TYPE_FATAL) {
                return true;
            }
        }
        return false;
    }

    /**
     * Generates a cost share entry and offset for the given entry and saves both to the valid group
     *
     * @param scrubbedEntry the originEntry that was scrubbed
     * @return a TransactionError initialized with any error encounted during entry generation, or (hopefully) null
     */
    protected TransactionError generateCostShareEntries(OriginEntryInformation scrubbedEntry,
            ScrubberReportData scrubberReport) {
        // 3000-COST-SHARE to 3100-READ-OFSD in the cobol Generate Cost Share Entries
        LOG.debug("generateCostShareEntries() started");
        try {
            OriginEntryFull costShareEntry = OriginEntryFull.copyFromOriginEntryable(scrubbedEntry);

            SystemOptions scrubbedEntryOption = accountingCycleCachingService
                    .getSystemOptions(scrubbedEntry.getUniversityFiscalYear());
            A21SubAccount scrubbedEntryA21SubAccount = accountingCycleCachingService.getA21SubAccount(
                    scrubbedEntry.getChartOfAccountsCode(), scrubbedEntry.getAccountNumber(),
                    scrubbedEntry.getSubAccountNumber());

            costShareEntry.setFinancialObjectCode(parameterService.getParameterValueAsString(ScrubberStep.class,
                    GeneralLedgerConstants.GlScrubberGroupParameters.COST_SHARE_OBJECT_CODE_PARM_NM));
            costShareEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
            costShareEntry
                    .setFinancialObjectTypeCode(scrubbedEntryOption.getFinancialObjectTypeTransferExpenseCd());
            costShareEntry.setTransactionLedgerEntrySequenceNumber(new Integer(0));

            StringBuffer description = new StringBuffer();
            description.append(costShareDescription);
            description.append(" ").append(scrubbedEntry.getAccountNumber());
            description.append(offsetString);
            costShareEntry.setTransactionLedgerEntryDescription(description.toString());

            costShareEntry.setTransactionLedgerEntryAmount(scrubCostShareAmount);
            if (scrubCostShareAmount.isPositive()) {
                costShareEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
            } else {
                costShareEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
                costShareEntry.setTransactionLedgerEntryAmount(scrubCostShareAmount.negated());
            }

            costShareEntry.setTransactionDate(runDate);
            costShareEntry.setOrganizationDocumentNumber(null);
            costShareEntry.setProjectCode(KFSConstants.getDashProjectCode());
            costShareEntry.setOrganizationReferenceId(null);
            costShareEntry.setReferenceFinancialDocumentTypeCode(null);
            costShareEntry.setReferenceFinancialSystemOriginationCode(null);
            costShareEntry.setReferenceFinancialDocumentNumber(null);
            costShareEntry.setFinancialDocumentReversalDate(null);
            costShareEntry.setTransactionEncumbranceUpdateCode(null);

            createOutputEntry(costShareEntry, OUTPUT_GLE_FILE_ps);
            scrubberReport.incrementCostShareEntryGenerated();

            OriginEntryFull costShareOffsetEntry = new OriginEntryFull(costShareEntry);
            costShareOffsetEntry.setTransactionLedgerEntryDescription(getOffsetMessage());
            OffsetDefinition offsetDefinition = accountingCycleCachingService.getOffsetDefinition(
                    scrubbedEntry.getUniversityFiscalYear(), scrubbedEntry.getChartOfAccountsCode(),
                    KFSConstants.TRANSFER_FUNDS, scrubbedEntry.getFinancialBalanceTypeCode());
            if (offsetDefinition != null) {
                if (offsetDefinition.getFinancialObject() == null) {
                    StringBuffer objectCodeKey = new StringBuffer();
                    objectCodeKey.append(offsetDefinition.getUniversityFiscalYear());
                    objectCodeKey.append("-").append(offsetDefinition.getChartOfAccountsCode());
                    objectCodeKey.append("-").append(offsetDefinition.getFinancialObjectCode());

                    Message m = new Message(configurationService
                            .getPropertyValueAsString(KFSKeyConstants.ERROR_OFFSET_DEFINITION_OBJECT_CODE_NOT_FOUND)
                            + " (" + objectCodeKey.toString() + ")", Message.TYPE_FATAL);
                    LOG.debug("generateCostShareEntries() Error 1 object not found");
                    return new TransactionError(costShareEntry, m);
                }

                costShareOffsetEntry.setFinancialObjectCode(offsetDefinition.getFinancialObjectCode());
                costShareOffsetEntry.setFinancialObject(offsetDefinition.getFinancialObject());
                costShareOffsetEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
            } else {
                Map<Transaction, List<Message>> errors = new HashMap<Transaction, List<Message>>();

                StringBuffer offsetKey = new StringBuffer("cost share transfer ");
                offsetKey.append(scrubbedEntry.getUniversityFiscalYear());
                offsetKey.append("-");
                offsetKey.append(scrubbedEntry.getChartOfAccountsCode());
                offsetKey.append("-TF-");
                offsetKey.append(scrubbedEntry.getFinancialBalanceTypeCode());

                Message m = new Message(configurationService.getPropertyValueAsString(
                        KFSKeyConstants.ERROR_OFFSET_DEFINITION_NOT_FOUND) + " (" + offsetKey.toString() + ")",
                        Message.TYPE_FATAL);

                LOG.debug("generateCostShareEntries() Error 2 offset not found");
                return new TransactionError(costShareEntry, m);
            }

            costShareOffsetEntry
                    .setFinancialObjectTypeCode(offsetDefinition.getFinancialObject().getFinancialObjectTypeCode());

            if (costShareEntry.isCredit()) {
                costShareOffsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
            } else {
                costShareOffsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
            }

            try {
                flexibleOffsetAccountService.updateOffset(costShareOffsetEntry);
            } catch (InvalidFlexibleOffsetException e) {
                Message m = new Message(e.getMessage(), Message.TYPE_FATAL);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("generateCostShareEntries() Cost Share Transfer Flexible Offset Error: "
                            + e.getMessage());
                }
                return new TransactionError(costShareEntry, m);
            }

            createOutputEntry(costShareOffsetEntry, OUTPUT_GLE_FILE_ps);
            scrubberReport.incrementCostShareEntryGenerated();

            OriginEntryFull costShareSourceAccountEntry = new OriginEntryFull(costShareEntry);

            description = new StringBuffer();
            description.append(costShareDescription);
            description.append(" ").append(scrubbedEntry.getAccountNumber());
            description.append(offsetString);
            costShareSourceAccountEntry.setTransactionLedgerEntryDescription(description.toString());

            costShareSourceAccountEntry
                    .setChartOfAccountsCode(scrubbedEntryA21SubAccount.getCostShareChartOfAccountCode());
            costShareSourceAccountEntry
                    .setAccountNumber(scrubbedEntryA21SubAccount.getCostShareSourceAccountNumber());

            setCostShareObjectCode(costShareSourceAccountEntry, scrubbedEntry);
            costShareSourceAccountEntry
                    .setSubAccountNumber(scrubbedEntryA21SubAccount.getCostShareSourceSubAccountNumber());

            if (StringHelper.isNullOrEmpty(costShareSourceAccountEntry.getSubAccountNumber())) {
                costShareSourceAccountEntry.setSubAccountNumber(KFSConstants.getDashSubAccountNumber());
            }

            costShareSourceAccountEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
            costShareSourceAccountEntry
                    .setFinancialObjectTypeCode(scrubbedEntryOption.getFinancialObjectTypeTransferExpenseCd());
            costShareSourceAccountEntry.setTransactionLedgerEntrySequenceNumber(new Integer(0));

            costShareSourceAccountEntry.setTransactionLedgerEntryAmount(scrubCostShareAmount);
            if (scrubCostShareAmount.isPositive()) {
                costShareSourceAccountEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
            } else {
                costShareSourceAccountEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
                costShareSourceAccountEntry.setTransactionLedgerEntryAmount(scrubCostShareAmount.negated());
            }

            costShareSourceAccountEntry.setTransactionDate(runDate);
            costShareSourceAccountEntry.setOrganizationDocumentNumber(null);
            costShareSourceAccountEntry.setProjectCode(KFSConstants.getDashProjectCode());
            costShareSourceAccountEntry.setOrganizationReferenceId(null);
            costShareSourceAccountEntry.setReferenceFinancialDocumentTypeCode(null);
            costShareSourceAccountEntry.setReferenceFinancialSystemOriginationCode(null);
            costShareSourceAccountEntry.setReferenceFinancialDocumentNumber(null);
            costShareSourceAccountEntry.setFinancialDocumentReversalDate(null);
            costShareSourceAccountEntry.setTransactionEncumbranceUpdateCode(null);

            createOutputEntry(costShareSourceAccountEntry, OUTPUT_GLE_FILE_ps);
            scrubberReport.incrementCostShareEntryGenerated();

            OriginEntryFull costShareSourceAccountOffsetEntry = new OriginEntryFull(costShareSourceAccountEntry);
            costShareSourceAccountOffsetEntry.setTransactionLedgerEntryDescription(getOffsetMessage());

            // Lookup the new offset definition.
            offsetDefinition = accountingCycleCachingService.getOffsetDefinition(
                    scrubbedEntry.getUniversityFiscalYear(), scrubbedEntry.getChartOfAccountsCode(),
                    KFSConstants.TRANSFER_FUNDS, scrubbedEntry.getFinancialBalanceTypeCode());
            if (offsetDefinition != null) {
                if (offsetDefinition.getFinancialObject() == null) {
                    Map<Transaction, List<Message>> errors = new HashMap<Transaction, List<Message>>();

                    StringBuffer objectCodeKey = new StringBuffer();
                    objectCodeKey.append(costShareEntry.getUniversityFiscalYear());
                    objectCodeKey.append("-").append(scrubbedEntry.getChartOfAccountsCode());
                    objectCodeKey.append("-").append(scrubbedEntry.getFinancialObjectCode());

                    Message m = new Message(configurationService
                            .getPropertyValueAsString(KFSKeyConstants.ERROR_OFFSET_DEFINITION_OBJECT_CODE_NOT_FOUND)
                            + " (" + objectCodeKey.toString() + ")", Message.TYPE_FATAL);

                    LOG.debug("generateCostShareEntries() Error 3 object not found");
                    return new TransactionError(costShareSourceAccountEntry, m);
                }

                costShareSourceAccountOffsetEntry.setFinancialObjectCode(offsetDefinition.getFinancialObjectCode());
                costShareSourceAccountOffsetEntry.setFinancialObject(offsetDefinition.getFinancialObject());
                costShareSourceAccountOffsetEntry
                        .setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
            } else {
                Map<Transaction, List<Message>> errors = new HashMap<Transaction, List<Message>>();

                StringBuffer offsetKey = new StringBuffer("cost share transfer source ");
                offsetKey.append(scrubbedEntry.getUniversityFiscalYear());
                offsetKey.append("-");
                offsetKey.append(scrubbedEntry.getChartOfAccountsCode());
                offsetKey.append("-TF-");
                offsetKey.append(scrubbedEntry.getFinancialBalanceTypeCode());

                Message m = new Message(configurationService.getPropertyValueAsString(
                        KFSKeyConstants.ERROR_OFFSET_DEFINITION_NOT_FOUND) + " (" + offsetKey.toString() + ")",
                        Message.TYPE_FATAL);

                LOG.debug("generateCostShareEntries() Error 4 offset not found");
                return new TransactionError(costShareSourceAccountEntry, m);
            }

            costShareSourceAccountOffsetEntry
                    .setFinancialObjectTypeCode(offsetDefinition.getFinancialObject().getFinancialObjectTypeCode());

            if (scrubbedEntry.isCredit()) {
                costShareSourceAccountOffsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
            } else {
                costShareSourceAccountOffsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
            }

            try {
                flexibleOffsetAccountService.updateOffset(costShareSourceAccountOffsetEntry);
            } catch (InvalidFlexibleOffsetException e) {
                Message m = new Message(e.getMessage(), Message.TYPE_FATAL);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("generateCostShareEntries() Cost Share Transfer Account Flexible Offset Error: "
                            + e.getMessage());
                }
                return new TransactionError(costShareEntry, m);
            }

            createOutputEntry(costShareSourceAccountOffsetEntry, OUTPUT_GLE_FILE_ps);
            scrubberReport.incrementCostShareEntryGenerated();

            scrubCostShareAmount = KualiDecimal.ZERO;
        } catch (IOException ioe) {
            LOG.error("generateCostShareEntries() Stopped: " + ioe.getMessage());
            throw new RuntimeException("generateCostShareEntries() Stopped: " + ioe.getMessage(), ioe);
        }
        LOG.debug("generateCostShareEntries() successful");
        return null;
    }

    /**
     * Get all the transaction descriptions from the param table
     */
    protected void setDescriptions() {
        //TODO: move to constants class?
        offsetDescription = "GENERATED OFFSET";
        capitalizationDescription = "GENERATED CAPITALIZATION";
        liabilityDescription = "GENERATED LIABILITY";
        costShareDescription = "GENERATED COST SHARE FROM";
        transferDescription = "GENERATED TRANSFER FROM";
    }

    /**
     * Generate the flag for the end of specific descriptions. This will be used in the demerger step
     */
    protected void setOffsetString() {

        NumberFormat nf = NumberFormat.getInstance();
        nf.setMaximumFractionDigits(0);
        nf.setMaximumIntegerDigits(2);
        nf.setMinimumFractionDigits(0);
        nf.setMinimumIntegerDigits(2);

        offsetString = COST_SHARE_TRANSFER_ENTRY_IND + nf.format(runCal.get(Calendar.MONTH) + 1)
                + nf.format(runCal.get(Calendar.DAY_OF_MONTH));
    }

    /**
     * Generate the offset message with the flag at the end
     *
     * @return a generated offset message
     */
    protected String getOffsetMessage() {
        String msg = offsetDescription + GeneralLedgerConstants.getSpaceTransactionLedgetEntryDescription();

        return msg.substring(0, OFFSET_MESSAGE_MAXLENGTH) + offsetString;
    }

    /**
     * Generates capitalization entries if necessary
     *
     * @param scrubbedEntry the entry to generate capitalization entries (possibly) for
     * @return null if no error, message if error
     */
    protected String processCapitalization(OriginEntryInformation scrubbedEntry,
            ScrubberReportData scrubberReport) {

        try {
            // Lines 4694 - 4798 of the Pro Cobol listing on Confluence
            if (!parameterService.getParameterValueAsBoolean(ScrubberStep.class,
                    GeneralLedgerConstants.GlScrubberGroupParameters.CAPITALIZATION_IND)) {
                return null;
            }

            OriginEntryFull capitalizationEntry = OriginEntryFull.copyFromOriginEntryable(scrubbedEntry);
            SystemOptions scrubbedEntryOption = accountingCycleCachingService
                    .getSystemOptions(scrubbedEntry.getUniversityFiscalYear());
            ObjectCode scrubbedEntryObjectCode = accountingCycleCachingService.getObjectCode(
                    scrubbedEntry.getUniversityFiscalYear(), scrubbedEntry.getChartOfAccountsCode(),
                    scrubbedEntry.getFinancialObjectCode());
            Chart scrubbedEntryChart = accountingCycleCachingService
                    .getChart(scrubbedEntry.getChartOfAccountsCode());
            Account scrubbedEntryAccount = accountingCycleCachingService
                    .getAccount(scrubbedEntry.getChartOfAccountsCode(), scrubbedEntry.getAccountNumber());

            ParameterEvaluator documentTypeCodes = (!ObjectUtils.isNull(scrubbedEntry))
                    ? /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                            ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.CAPITALIZATION_DOC_TYPE_CODES,
                            scrubbedEntry.getFinancialDocumentTypeCode())
                    : null;
            ParameterEvaluator fiscalPeriodCodes = (!ObjectUtils.isNull(scrubbedEntry))
                    ? /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                            ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.CAPITALIZATION_FISCAL_PERIOD_CODES,
                            scrubbedEntry.getUniversityFiscalPeriodCode())
                    : null;
            ParameterEvaluator objectSubTypeCodes = (!ObjectUtils.isNull(scrubbedEntryObjectCode))
                    ? /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                            ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.CAPITALIZATION_OBJ_SUB_TYPE_CODES,
                            scrubbedEntryObjectCode.getFinancialObjectSubTypeCode())
                    : null;
            ParameterEvaluator subFundGroupCodes = (!ObjectUtils.isNull(scrubbedEntryAccount))
                    ? /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                            ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.CAPITALIZATION_SUB_FUND_GROUP_CODES,
                            scrubbedEntryAccount.getSubFundGroupCode())
                    : null;
            ParameterEvaluator chartCodes = (!ObjectUtils.isNull(scrubbedEntry)) ? /*REFACTORME*/SpringContext
                    .getBean(ParameterEvaluatorService.class).getParameterEvaluator(ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.CAPITALIZATION_CHART_CODES,
                            scrubbedEntry.getChartOfAccountsCode())
                    : null;

            if (scrubbedEntry.getFinancialBalanceTypeCode()
                    .equals(scrubbedEntryOption.getActualFinancialBalanceTypeCd())
                    && scrubbedEntry.getUniversityFiscalYear().intValue() > 1995
                    && (documentTypeCodes != null && documentTypeCodes.evaluationSucceeds())
                    && (fiscalPeriodCodes != null && fiscalPeriodCodes.evaluationSucceeds())
                    && (objectSubTypeCodes != null && objectSubTypeCodes.evaluationSucceeds())
                    && (subFundGroupCodes != null && subFundGroupCodes.evaluationSucceeds())
                    && (chartCodes != null && chartCodes.evaluationSucceeds())) {

                String objectSubTypeCode = scrubbedEntryObjectCode.getFinancialObjectSubTypeCode();

                String capitalizationObjectCode = parameterService.getSubParameterValueAsString(ScrubberStep.class,
                        GeneralLedgerConstants.GlScrubberGroupParameters.CAPITALIZATION_SUBTYPE_OBJECT,
                        objectSubTypeCode);
                if (org.apache.commons.lang.StringUtils.isNotBlank(capitalizationObjectCode)) {
                    capitalizationEntry.setFinancialObjectCode(capitalizationObjectCode);
                    capitalizationEntry.setFinancialObject(accountingCycleCachingService.getObjectCode(
                            capitalizationEntry.getUniversityFiscalYear(),
                            capitalizationEntry.getChartOfAccountsCode(),
                            capitalizationEntry.getFinancialObjectCode()));
                }

                // financialSubObjectCode should always be changed to dashes for capitalization entries
                capitalizationEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());

                capitalizationEntry
                        .setFinancialObjectTypeCode(scrubbedEntryOption.getFinancialObjectTypeAssetsCd());
                capitalizationEntry.setTransactionLedgerEntryDescription(capitalizationDescription);

                plantFundAccountLookup(scrubbedEntry, capitalizationEntry);

                capitalizationEntry.setUniversityFiscalPeriodCode(scrubbedEntry.getUniversityFiscalPeriodCode());

                createOutputEntry(capitalizationEntry, OUTPUT_GLE_FILE_ps);
                scrubberReport.incrementCapitalizationEntryGenerated();

                // Clear out the id & the ojb version number to make sure we do an insert on the next one
                capitalizationEntry.setVersionNumber(null);
                capitalizationEntry.setEntryId(null);

                // Check system parameters for overriding fund balance object code; otherwise, use
                // the chart fund balance object code.
                String fundBalanceCode = parameterService.getParameterValueAsString(ScrubberStep.class,
                        GlParameterConstants.CAPITALIZATION_OFFSET_CODE);

                ObjectCode fundObjectCode = getFundBalanceObjectCode(fundBalanceCode, capitalizationEntry);

                if (fundObjectCode != null) {
                    capitalizationEntry.setFinancialObjectTypeCode(fundObjectCode.getFinancialObjectTypeCode());
                    capitalizationEntry.setFinancialObjectCode(fundBalanceCode);
                } else {
                    capitalizationEntry.setFinancialObjectCode(scrubbedEntryChart.getFundBalanceObjectCode());
                    //TODO: check to see if COBOL does this - seems weird - is this saying if the object code doesn't exist use the value from options?  Shouldn't it always come from one or the other?
                    if (ObjectUtils.isNotNull(scrubbedEntryChart.getFundBalanceObject())) {
                        capitalizationEntry.setFinancialObjectTypeCode(
                                scrubbedEntryChart.getFundBalanceObject().getFinancialObjectTypeCode());
                    } else {
                        capitalizationEntry
                                .setFinancialObjectTypeCode(scrubbedEntryOption.getFinObjectTypeFundBalanceCd());
                    }
                }

                populateTransactionDebtCreditCode(scrubbedEntry, capitalizationEntry);

                try {
                    flexibleOffsetAccountService.updateOffset(capitalizationEntry);
                } catch (InvalidFlexibleOffsetException e) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug(
                                "processCapitalization() Capitalization Flexible Offset Error: " + e.getMessage());
                    }
                    return e.getMessage();
                }

                createOutputEntry(capitalizationEntry, OUTPUT_GLE_FILE_ps);
                scrubberReport.incrementCapitalizationEntryGenerated();
            }
        } catch (IOException ioe) {
            LOG.error("processCapitalization() Stopped: " + ioe.getMessage());
            throw new RuntimeException("processCapitalization() Stopped: " + ioe.getMessage(), ioe);
        }
        return null;
    }

    /**
     * Generates the plant indebtedness entries
     *
     * @param scrubbedEntry the entry to generated plant indebtedness entries for if necessary
     * @return null if no error, message if error
     */
    protected String processPlantIndebtedness(OriginEntryInformation scrubbedEntry,
            ScrubberReportData scrubberReport) {
        try {
            // Lines 4855 - 4979 of the Pro Cobol listing on Confluence
            if (!parameterService.getParameterValueAsBoolean(ScrubberStep.class,
                    GeneralLedgerConstants.GlScrubberGroupParameters.PLANT_INDEBTEDNESS_IND)) {
                return null;
            }

            OriginEntryFull plantIndebtednessEntry = OriginEntryFull.copyFromOriginEntryable(scrubbedEntry);

            SystemOptions scrubbedEntryOption = accountingCycleCachingService
                    .getSystemOptions(scrubbedEntry.getUniversityFiscalYear());
            ObjectCode scrubbedEntryObjectCode = accountingCycleCachingService.getObjectCode(
                    scrubbedEntry.getUniversityFiscalYear(), scrubbedEntry.getChartOfAccountsCode(),
                    scrubbedEntry.getFinancialObjectCode());
            Account scrubbedEntryAccount = accountingCycleCachingService
                    .getAccount(scrubbedEntry.getChartOfAccountsCode(), scrubbedEntry.getAccountNumber());
            Chart scrubbedEntryChart = accountingCycleCachingService
                    .getChart(scrubbedEntry.getChartOfAccountsCode());
            if (!ObjectUtils.isNull(scrubbedEntryAccount)) {
                scrubbedEntryAccount.setOrganization(accountingCycleCachingService.getOrganization(
                        scrubbedEntryAccount.getChartOfAccountsCode(), scrubbedEntryAccount.getOrganizationCode()));
            }

            ParameterEvaluator objectSubTypeCodes = (!ObjectUtils.isNull(scrubbedEntryObjectCode))
                    ? /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                            ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.PLANT_INDEBTEDNESS_OBJ_SUB_TYPE_CODES,
                            scrubbedEntryObjectCode.getFinancialObjectSubTypeCode())
                    : null;
            ParameterEvaluator subFundGroupCodes = (!ObjectUtils.isNull(scrubbedEntryAccount))
                    ? /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                            ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.PLANT_INDEBTEDNESS_SUB_FUND_GROUP_CODES,
                            scrubbedEntryAccount.getSubFundGroupCode())
                    : null;

            if (scrubbedEntry.getFinancialBalanceTypeCode()
                    .equals(scrubbedEntryOption.getActualFinancialBalanceTypeCd())
                    && (subFundGroupCodes != null && subFundGroupCodes.evaluationSucceeds())
                    && (objectSubTypeCodes != null && objectSubTypeCodes.evaluationSucceeds())) {

                plantIndebtednessEntry
                        .setTransactionLedgerEntryDescription(KFSConstants.PLANT_INDEBTEDNESS_ENTRY_DESCRIPTION);
                populateTransactionDebtCreditCode(scrubbedEntry, plantIndebtednessEntry);

                plantIndebtednessEntry.setTransactionScrubberOffsetGenerationIndicator(true);
                createOutputEntry(plantIndebtednessEntry, OUTPUT_GLE_FILE_ps);
                scrubberReport.incrementPlantIndebtednessEntryGenerated();

                // Clear out the id & the ojb version number to make sure we do an insert on the next one
                plantIndebtednessEntry.setVersionNumber(null);
                plantIndebtednessEntry.setEntryId(null);

                // Check system parameters for overriding fund balance object code; otherwise, use
                // the chart fund balance object code.
                String fundBalanceCode = parameterService.getParameterValueAsString(ScrubberStep.class,
                        GlParameterConstants.PLANT_INDEBTEDNESS_OFFSET_CODE);

                ObjectCode fundObjectCode = getFundBalanceObjectCode(fundBalanceCode, plantIndebtednessEntry);
                if (fundObjectCode != null) {
                    plantIndebtednessEntry.setFinancialObjectTypeCode(fundObjectCode.getFinancialObjectTypeCode());
                    plantIndebtednessEntry.setFinancialObjectCode(fundBalanceCode);
                } else {
                    plantIndebtednessEntry
                            .setFinancialObjectTypeCode(scrubbedEntryOption.getFinObjectTypeFundBalanceCd());
                    plantIndebtednessEntry.setFinancialObjectCode(scrubbedEntryChart.getFundBalanceObjectCode());
                }

                plantIndebtednessEntry.setTransactionDebitCreditCode(scrubbedEntry.getTransactionDebitCreditCode());

                plantIndebtednessEntry.setTransactionScrubberOffsetGenerationIndicator(true);
                plantIndebtednessEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());

                try {
                    flexibleOffsetAccountService.updateOffset(plantIndebtednessEntry);
                } catch (InvalidFlexibleOffsetException e) {
                    LOG.error("processPlantIndebtedness() Flexible Offset Exception (1)", e);
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("processPlantIndebtedness() Plant Indebtedness Flexible Offset Error: "
                                + e.getMessage());
                    }
                    return e.getMessage();
                }

                createOutputEntry(plantIndebtednessEntry, OUTPUT_GLE_FILE_ps);
                scrubberReport.incrementPlantIndebtednessEntryGenerated();

                // Clear out the id & the ojb version number to make sure we do an insert on the next one
                plantIndebtednessEntry.setVersionNumber(null);
                plantIndebtednessEntry.setEntryId(null);

                plantIndebtednessEntry.setFinancialObjectCode(scrubbedEntry.getFinancialObjectCode());
                plantIndebtednessEntry.setFinancialObjectTypeCode(scrubbedEntry.getFinancialObjectTypeCode());
                plantIndebtednessEntry.setTransactionDebitCreditCode(scrubbedEntry.getTransactionDebitCreditCode());

                plantIndebtednessEntry
                        .setTransactionLedgerEntryDescription(scrubbedEntry.getTransactionLedgerEntryDescription());

                plantIndebtednessEntry.setAccountNumber(scrubbedEntry.getAccountNumber());
                plantIndebtednessEntry.setSubAccountNumber(scrubbedEntry.getSubAccountNumber());

                plantIndebtednessEntry
                        .setAccountNumber(scrubbedEntryAccount.getOrganization().getCampusPlantAccountNumber());
                plantIndebtednessEntry
                        .setChartOfAccountsCode(scrubbedEntryAccount.getOrganization().getCampusPlantChartCode());

                plantIndebtednessEntry.setSubAccountNumber(KFSConstants.getDashSubAccountNumber());
                plantIndebtednessEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());

                StringBuffer litGenPlantXferFrom = new StringBuffer();
                litGenPlantXferFrom.append(transferDescription + " ");
                litGenPlantXferFrom.append(scrubbedEntry.getChartOfAccountsCode()).append(" ");
                litGenPlantXferFrom.append(scrubbedEntry.getAccountNumber());
                plantIndebtednessEntry.setTransactionLedgerEntryDescription(litGenPlantXferFrom.toString());

                createOutputEntry(plantIndebtednessEntry, OUTPUT_GLE_FILE_ps);
                scrubberReport.incrementPlantIndebtednessEntryGenerated();

                // Clear out the id & the ojb version number to make sure we do an insert on the next one
                plantIndebtednessEntry.setVersionNumber(null);
                plantIndebtednessEntry.setEntryId(null);

                plantIndebtednessEntry.setFinancialObjectCode(scrubbedEntryChart.getFundBalanceObjectCode());
                plantIndebtednessEntry
                        .setFinancialObjectTypeCode(scrubbedEntryOption.getFinObjectTypeFundBalanceCd());
                plantIndebtednessEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());

                populateTransactionDebtCreditCode(scrubbedEntry, plantIndebtednessEntry);

                try {
                    flexibleOffsetAccountService.updateOffset(plantIndebtednessEntry);
                } catch (InvalidFlexibleOffsetException e) {
                    LOG.error("processPlantIndebtedness() Flexible Offset Exception (2)", e);
                    LOG.debug("processPlantIndebtedness() Plant Indebtedness Flexible Offset Error: "
                            + e.getMessage());
                    return e.getMessage();
                }

                createOutputEntry(plantIndebtednessEntry, OUTPUT_GLE_FILE_ps);
                scrubberReport.incrementPlantIndebtednessEntryGenerated();
            }
        } catch (IOException ioe) {
            LOG.error("processPlantIndebtedness() Stopped: " + ioe.getMessage());
            throw new RuntimeException("processPlantIndebtedness() Stopped: " + ioe.getMessage(), ioe);
        }
        return null;
    }

    /**
     * Generate the liability entries for the entry if necessary
     *
     * @param scrubbedEntry the entry to generate liability entries for if necessary
     * @return null if no error, message if error
     */
    protected String processLiabilities(OriginEntryInformation scrubbedEntry, ScrubberReportData scrubberReport) {
        try {
            // Lines 4799 to 4839 of the Pro Cobol list of the scrubber on Confluence
            if (!parameterService.getParameterValueAsBoolean(ScrubberStep.class,
                    GeneralLedgerConstants.GlScrubberGroupParameters.LIABILITY_IND)) {
                return null;
            }

            Chart scrubbedEntryChart = accountingCycleCachingService
                    .getChart(scrubbedEntry.getChartOfAccountsCode());
            SystemOptions scrubbedEntryOption = accountingCycleCachingService
                    .getSystemOptions(scrubbedEntry.getUniversityFiscalYear());
            ObjectCode scrubbedEntryFinancialObject = accountingCycleCachingService.getObjectCode(
                    scrubbedEntry.getUniversityFiscalYear(), scrubbedEntry.getChartOfAccountsCode(),
                    scrubbedEntry.getFinancialObjectCode());
            Account scrubbedEntryAccount = accountingCycleCachingService
                    .getAccount(scrubbedEntry.getChartOfAccountsCode(), scrubbedEntry.getAccountNumber());

            ParameterEvaluator chartCodes = (!ObjectUtils.isNull(scrubbedEntry))
                    ? /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                            ScrubberStep.class, GeneralLedgerConstants.GlScrubberGroupRules.LIABILITY_CHART_CODES,
                            scrubbedEntry.getChartOfAccountsCode())
                    : null;
            ParameterEvaluator docTypeCodes = (!ObjectUtils.isNull(scrubbedEntry)) ? /*REFACTORME*/SpringContext
                    .getBean(ParameterEvaluatorService.class).getParameterEvaluator(ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.LIABILITY_DOC_TYPE_CODES,
                            scrubbedEntry.getFinancialDocumentTypeCode())
                    : null;
            ParameterEvaluator fiscalPeriods = (!ObjectUtils.isNull(scrubbedEntry)) ? /*REFACTORME*/SpringContext
                    .getBean(ParameterEvaluatorService.class).getParameterEvaluator(ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.LIABILITY_FISCAL_PERIOD_CODES,
                            scrubbedEntry.getUniversityFiscalPeriodCode())
                    : null;
            ParameterEvaluator objSubTypeCodes = (!ObjectUtils.isNull(scrubbedEntryFinancialObject))
                    ? /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                            ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.LIABILITY_OBJ_SUB_TYPE_CODES,
                            scrubbedEntryFinancialObject.getFinancialObjectSubTypeCode())
                    : null;
            ParameterEvaluator subFundGroupCodes = (!ObjectUtils.isNull(scrubbedEntryAccount))
                    ? /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                            ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.LIABILITY_SUB_FUND_GROUP_CODES,
                            scrubbedEntryAccount.getSubFundGroupCode())
                    : null;

            if (scrubbedEntry.getFinancialBalanceTypeCode()
                    .equals(scrubbedEntryOption.getActualFinancialBalanceTypeCd())
                    && scrubbedEntry.getUniversityFiscalYear().intValue() > 1995
                    && (docTypeCodes != null && docTypeCodes.evaluationSucceeds())
                    && (fiscalPeriods != null && fiscalPeriods.evaluationSucceeds())
                    && (objSubTypeCodes != null && objSubTypeCodes.evaluationSucceeds())
                    && (subFundGroupCodes != null && subFundGroupCodes.evaluationSucceeds())
                    && (chartCodes != null && chartCodes.evaluationSucceeds())) {
                OriginEntryFull liabilityEntry = OriginEntryFull.copyFromOriginEntryable(scrubbedEntry);

                liabilityEntry.setFinancialObjectCode(parameterService.getParameterValueAsString(ScrubberStep.class,
                        GeneralLedgerConstants.GlScrubberGroupParameters.LIABILITY_OBJECT_CODE));
                liabilityEntry.setFinancialObjectTypeCode(scrubbedEntryOption.getFinObjectTypeLiabilitiesCode());

                liabilityEntry.setTransactionDebitCreditCode(scrubbedEntry.getTransactionDebitCreditCode());
                liabilityEntry.setTransactionLedgerEntryDescription(liabilityDescription);
                plantFundAccountLookup(scrubbedEntry, liabilityEntry);

                createOutputEntry(liabilityEntry, OUTPUT_GLE_FILE_ps);
                scrubberReport.incrementLiabilityEntryGenerated();

                // Clear out the id & the ojb version number to make sure we do an insert on the next one
                liabilityEntry.setVersionNumber(null);
                liabilityEntry.setEntryId(null);

                // Check system parameters for overriding fund balance object code; otherwise, use
                // the chart fund balance object code.
                String fundBalanceCode = parameterService.getParameterValueAsString(ScrubberStep.class,
                        GlParameterConstants.LIABILITY_OFFSET_CODE);

                ObjectCode fundObjectCode = getFundBalanceObjectCode(fundBalanceCode, liabilityEntry);
                if (fundObjectCode != null) {
                    liabilityEntry.setFinancialObjectTypeCode(fundObjectCode.getFinancialObjectTypeCode());
                    liabilityEntry.setFinancialObjectCode(fundBalanceCode);
                } else {
                    // ... and now generate the offset half of the liability entry
                    liabilityEntry.setFinancialObjectCode(scrubbedEntryChart.getFundBalanceObjectCode());
                    if (ObjectUtils.isNotNull(scrubbedEntryChart.getFundBalanceObject())) {
                        liabilityEntry.setFinancialObjectTypeCode(
                                scrubbedEntryChart.getFundBalanceObject().getFinancialObjectTypeCode());
                    } else {
                        liabilityEntry
                                .setFinancialObjectTypeCode(scrubbedEntryOption.getFinObjectTypeFundBalanceCd());
                    }
                }

                if (liabilityEntry.isDebit()) {
                    liabilityEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
                } else {
                    liabilityEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
                }

                try {
                    flexibleOffsetAccountService.updateOffset(liabilityEntry);
                } catch (InvalidFlexibleOffsetException e) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("processLiabilities() Liability Flexible Offset Error: " + e.getMessage());
                    }
                    return e.getMessage();
                }

                createOutputEntry(liabilityEntry, OUTPUT_GLE_FILE_ps);
                scrubberReport.incrementLiabilityEntryGenerated();
            }
        } catch (IOException ioe) {
            LOG.error("processLiabilities() Stopped: " + ioe.getMessage());
            throw new RuntimeException("processLiabilities() Stopped: " + ioe.getMessage(), ioe);
        }
        return null;
    }

    /**
     *
     * This method...
     * @param fundBalanceCodeParameter
     * @param originEntryFull
     * @return
     */
    protected ObjectCode getFundBalanceObjectCode(String fundBalanceCode, OriginEntryFull originEntryFull) {
        ObjectCode fundBalanceObjectCode = null;
        if (fundBalanceCode != null) {
            Map<String, Object> criteriaMap = new HashMap<String, Object>();
            criteriaMap.put("universityFiscalYear", originEntryFull.getUniversityFiscalYear());
            criteriaMap.put("chartOfAccountsCode", originEntryFull.getChartOfAccountsCode());
            criteriaMap.put("financialObjectCode", fundBalanceCode);

            fundBalanceObjectCode = businessObjectService.findByPrimaryKey(ObjectCode.class, criteriaMap);
        }

        return fundBalanceObjectCode;
    }

    /**
     *
     * This method...
     * @param scrubbedEntry
     * @param fullEntry
     */
    protected void populateTransactionDebtCreditCode(OriginEntryInformation scrubbedEntry,
            OriginEntryFull fullEntry) {
        if (scrubbedEntry.isDebit()) {
            fullEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
        } else {
            fullEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
        }
    }

    /**
     * Updates the entries with the proper chart and account for the plant fund
     *
     * @param scrubbedEntry basis for plant fund entry
     * @param liabilityEntry liability entry
     */
    protected void plantFundAccountLookup(OriginEntryInformation scrubbedEntry, OriginEntryFull liabilityEntry) {
        // 4000-PLANT-FUND-ACCT to 4000-PLANT-FUND-ACCT-EXIT in cobol

        liabilityEntry.setSubAccountNumber(KFSConstants.getDashSubAccountNumber());
        ObjectCode scrubbedEntryObjectCode = accountingCycleCachingService.getObjectCode(
                scrubbedEntry.getUniversityFiscalYear(), scrubbedEntry.getChartOfAccountsCode(),
                scrubbedEntry.getFinancialObjectCode());
        Account scrubbedEntryAccount = accountingCycleCachingService
                .getAccount(scrubbedEntry.getChartOfAccountsCode(), scrubbedEntry.getAccountNumber());
        scrubbedEntryAccount.setOrganization(accountingCycleCachingService.getOrganization(
                scrubbedEntryAccount.getChartOfAccountsCode(), scrubbedEntryAccount.getOrganizationCode()));

        if (!ObjectUtils.isNull(scrubbedEntryAccount) && !ObjectUtils.isNull(scrubbedEntryObjectCode)) {
            String objectSubTypeCode = scrubbedEntryObjectCode.getFinancialObjectSubTypeCode();
            ParameterEvaluator campusObjSubTypeCodes = /*REFACTORME*/SpringContext
                    .getBean(ParameterEvaluatorService.class).getParameterEvaluator(ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.PLANT_FUND_CAMPUS_OBJECT_SUB_TYPE_CODES,
                            objectSubTypeCode);
            ParameterEvaluator orgObjSubTypeCodes = /*REFACTORME*/SpringContext
                    .getBean(ParameterEvaluatorService.class).getParameterEvaluator(ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.PLANT_FUND_ORG_OBJECT_SUB_TYPE_CODES,
                            objectSubTypeCode);

            if (campusObjSubTypeCodes.evaluationSucceeds()) {
                liabilityEntry
                        .setAccountNumber(scrubbedEntryAccount.getOrganization().getCampusPlantAccountNumber());
                liabilityEntry
                        .setChartOfAccountsCode(scrubbedEntryAccount.getOrganization().getCampusPlantChartCode());
            } else if (orgObjSubTypeCodes.evaluationSucceeds()) {
                liabilityEntry.setAccountNumber(
                        scrubbedEntryAccount.getOrganization().getOrganizationPlantAccountNumber());
                liabilityEntry.setChartOfAccountsCode(
                        scrubbedEntryAccount.getOrganization().getOrganizationPlantChartCode());
            }
        }
    }

    /**
     * The purpose of this method is to generate a "Cost Share Encumbrance"
     * transaction for the current transaction and its offset. The cost share chart and account for current transaction are obtained
     * from the CA_A21_SUB_ACCT_T table. This method calls the method SET-OBJECT-2004 to get the Cost Share Object Code. It then
     * writes out the cost share transaction. Next it read the GL_OFFSET_DEFN_T table for the offset object code that corresponds to
     * the cost share object code. In addition to the object code it needs to get subobject code. It then reads the CA_OBJECT_CODE_T
     * table to make sure the offset object code found in the GL_OFFSET_DEFN_T is valid and to get the object type code associated
     * with this object code. It writes out the offset transaction and returns.
     *
     * @param scrubbedEntry the entry to perhaps create a cost share encumbrance for
     * @return a message if there was an error encountered generating the entries, or (hopefully) null if no errors were encountered
     */
    protected TransactionError generateCostShareEncumbranceEntries(OriginEntryInformation scrubbedEntry,
            ScrubberReportData scrubberReport) {
        try {
            // 3200-COST-SHARE-ENC to 3200-CSE-EXIT in the COBOL
            LOG.debug("generateCostShareEncumbranceEntries() started");

            OriginEntryFull costShareEncumbranceEntry = OriginEntryFull.copyFromOriginEntryable(scrubbedEntry);

            // First 28 characters of the description, padding to 28 if shorter)
            StringBuffer buffer = new StringBuffer((scrubbedEntry.getTransactionLedgerEntryDescription()
                    + GeneralLedgerConstants.getSpaceTransactionLedgetEntryDescription()).substring(0,
                            COST_SHARE_ENCUMBRANCE_ENTRY_MAXLENGTH));

            buffer.append("FR-");
            buffer.append(costShareEncumbranceEntry.getChartOfAccountsCode());
            buffer.append(costShareEncumbranceEntry.getAccountNumber());

            costShareEncumbranceEntry.setTransactionLedgerEntryDescription(buffer.toString());

            A21SubAccount scrubbedEntryA21SubAccount = accountingCycleCachingService.getA21SubAccount(
                    scrubbedEntry.getChartOfAccountsCode(), scrubbedEntry.getAccountNumber(),
                    scrubbedEntry.getSubAccountNumber());
            SystemOptions scrubbedEntryOption = accountingCycleCachingService
                    .getSystemOptions(scrubbedEntry.getUniversityFiscalYear());

            costShareEncumbranceEntry
                    .setChartOfAccountsCode(scrubbedEntryA21SubAccount.getCostShareChartOfAccountCode());
            costShareEncumbranceEntry
                    .setAccountNumber(scrubbedEntryA21SubAccount.getCostShareSourceAccountNumber());
            costShareEncumbranceEntry
                    .setSubAccountNumber(scrubbedEntryA21SubAccount.getCostShareSourceSubAccountNumber());

            if (!StringUtils.hasText(costShareEncumbranceEntry.getSubAccountNumber())) {
                costShareEncumbranceEntry.setSubAccountNumber(KFSConstants.getDashSubAccountNumber());
            }

            costShareEncumbranceEntry
                    .setFinancialBalanceTypeCode(scrubbedEntryOption.getCostShareEncumbranceBalanceTypeCd());
            setCostShareObjectCode(costShareEncumbranceEntry, scrubbedEntry);
            costShareEncumbranceEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
            costShareEncumbranceEntry.setTransactionLedgerEntrySequenceNumber(new Integer(0));

            if (!StringUtils.hasText(scrubbedEntry.getTransactionDebitCreditCode())) {
                if (scrubbedEntry.getTransactionLedgerEntryAmount().isPositive()) {
                    costShareEncumbranceEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
                } else {
                    costShareEncumbranceEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
                    costShareEncumbranceEntry.setTransactionLedgerEntryAmount(
                            scrubbedEntry.getTransactionLedgerEntryAmount().negated());
                }
            }

            costShareEncumbranceEntry.setTransactionDate(runDate);

            costShareEncumbranceEntry.setTransactionScrubberOffsetGenerationIndicator(true);
            createOutputEntry(costShareEncumbranceEntry, OUTPUT_GLE_FILE_ps);
            scrubberReport.incrementCostShareEncumbranceGenerated();

            OriginEntryFull costShareEncumbranceOffsetEntry = new OriginEntryFull(costShareEncumbranceEntry);
            costShareEncumbranceOffsetEntry.setTransactionLedgerEntryDescription(offsetDescription);
            OffsetDefinition offset = accountingCycleCachingService.getOffsetDefinition(
                    costShareEncumbranceEntry.getUniversityFiscalYear(),
                    costShareEncumbranceEntry.getChartOfAccountsCode(),
                    costShareEncumbranceEntry.getFinancialDocumentTypeCode(),
                    costShareEncumbranceEntry.getFinancialBalanceTypeCode());

            if (offset != null) {
                if (offset.getFinancialObject() == null) {
                    StringBuffer offsetKey = new StringBuffer();
                    offsetKey.append(offset.getUniversityFiscalYear());
                    offsetKey.append("-");
                    offsetKey.append(offset.getChartOfAccountsCode());
                    offsetKey.append("-");
                    offsetKey.append(offset.getFinancialObjectCode());

                    LOG.debug("generateCostShareEncumbranceEntries() object code not found");
                    return new TransactionError(costShareEncumbranceEntry,
                            new Message(configurationService
                                    .getPropertyValueAsString(KFSKeyConstants.ERROR_NO_OBJECT_FOR_OBJECT_ON_OFSD)
                                    + "(" + offsetKey.toString() + ")", Message.TYPE_FATAL));
                }
                costShareEncumbranceOffsetEntry.setFinancialObjectCode(offset.getFinancialObjectCode());
                costShareEncumbranceOffsetEntry.setFinancialObject(offset.getFinancialObject());
                costShareEncumbranceOffsetEntry
                        .setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
            } else {
                StringBuffer offsetKey = new StringBuffer("Cost share encumbrance ");
                offsetKey.append(costShareEncumbranceEntry.getUniversityFiscalYear());
                offsetKey.append("-");
                offsetKey.append(costShareEncumbranceEntry.getChartOfAccountsCode());
                offsetKey.append("-");
                offsetKey.append(costShareEncumbranceEntry.getFinancialDocumentTypeCode());
                offsetKey.append("-");
                offsetKey.append(costShareEncumbranceEntry.getFinancialBalanceTypeCode());

                LOG.debug("generateCostShareEncumbranceEntries() offset not found");
                return new TransactionError(costShareEncumbranceEntry,
                        new Message(configurationService
                                .getPropertyValueAsString(KFSKeyConstants.ERROR_OFFSET_DEFINITION_NOT_FOUND) + "("
                                + offsetKey.toString() + ")", Message.TYPE_FATAL));
            }

            costShareEncumbranceOffsetEntry
                    .setFinancialObjectTypeCode(offset.getFinancialObject().getFinancialObjectTypeCode());

            if (costShareEncumbranceEntry.isCredit()) {
                costShareEncumbranceOffsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
            } else {
                costShareEncumbranceOffsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
            }

            costShareEncumbranceOffsetEntry.setTransactionDate(runDate);
            costShareEncumbranceOffsetEntry.setOrganizationDocumentNumber(null);
            costShareEncumbranceOffsetEntry.setProjectCode(KFSConstants.getDashProjectCode());
            costShareEncumbranceOffsetEntry.setOrganizationReferenceId(null);
            costShareEncumbranceOffsetEntry.setReferenceFinancialDocumentTypeCode(null);
            costShareEncumbranceOffsetEntry.setReferenceFinancialSystemOriginationCode(null);
            costShareEncumbranceOffsetEntry.setReferenceFinancialDocumentNumber(null);
            costShareEncumbranceOffsetEntry.setReversalDate(null);
            costShareEncumbranceOffsetEntry.setTransactionEncumbranceUpdateCode(null);

            costShareEncumbranceOffsetEntry.setTransactionScrubberOffsetGenerationIndicator(true);

            try {
                flexibleOffsetAccountService.updateOffset(costShareEncumbranceOffsetEntry);
            } catch (InvalidFlexibleOffsetException e) {
                Message m = new Message(e.getMessage(), Message.TYPE_FATAL);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("generateCostShareEncumbranceEntries() Cost Share Encumbrance Flexible Offset Error: "
                            + e.getMessage());
                }
                return new TransactionError(costShareEncumbranceOffsetEntry, m);
            }

            createOutputEntry(costShareEncumbranceOffsetEntry, OUTPUT_GLE_FILE_ps);
            scrubberReport.incrementCostShareEncumbranceGenerated();
        } catch (IOException ioe) {
            LOG.error("generateCostShareEncumbranceEntries() Stopped: " + ioe.getMessage());
            throw new RuntimeException("generateCostShareEncumbranceEntries() Stopped: " + ioe.getMessage(), ioe);
        }
        LOG.debug("generateCostShareEncumbranceEntries() returned successfully");
        return null;
    }

    /**
     * Sets the proper cost share object code in an entry and its offset
     *
     * @param costShareEntry GL Entry for cost share
     * @param originEntry Scrubbed GL Entry that this is based on
     */
    @Override
    public void setCostShareObjectCode(OriginEntryFull costShareEntry, OriginEntryInformation originEntry) {
        ObjectCode originEntryFinancialObject = accountingCycleCachingService.getObjectCode(
                originEntry.getUniversityFiscalYear(), originEntry.getChartOfAccountsCode(),
                originEntry.getFinancialObjectCode());

        if (originEntryFinancialObject == null) {
            addTransactionError(
                    configurationService.getPropertyValueAsString(KFSKeyConstants.ERROR_OBJECT_CODE_NOT_FOUND),
                    originEntry.getFinancialObjectCode(), Message.TYPE_FATAL);
        }

        String originEntryObjectLevelCode = (originEntryFinancialObject == null) ? ""
                : originEntryFinancialObject.getFinancialObjectLevelCode();

        String financialOriginEntryObjectCode = originEntry.getFinancialObjectCode();
        //String originEntryObjectCode = scrubberProcessObjectCodeOverride.getOriginEntryObjectCode(originEntryObjectLevelCode, financialOriginEntryObjectCode);

        // General rules
        String param = parameterService.getSubParameterValueAsString(ScrubberStep.class,
                GeneralLedgerConstants.GlScrubberGroupParameters.COST_SHARE_OBJECT_CODE_BY_LEVEL_PARM_NM,
                originEntryObjectLevelCode);
        if (param == null) {
            param = parameterService.getSubParameterValueAsString(ScrubberStep.class,
                    GeneralLedgerConstants.GlScrubberGroupParameters.COST_SHARE_OBJECT_CODE_BY_LEVEL_PARM_NM,
                    "DEFAULT");
            if (param == null) {
                throw new RuntimeException(
                        "Unable to determine cost sharing object code from object level.  Default entry missing.");
            }
        }
        financialOriginEntryObjectCode = param;

        // Lookup the new object code
        ObjectCode objectCode = accountingCycleCachingService.getObjectCode(
                costShareEntry.getUniversityFiscalYear(), costShareEntry.getChartOfAccountsCode(),
                financialOriginEntryObjectCode);
        if (objectCode != null) {
            costShareEntry.setFinancialObjectTypeCode(objectCode.getFinancialObjectTypeCode());
            costShareEntry.setFinancialObjectCode(financialOriginEntryObjectCode);
        } else {
            addTransactionError(
                    configurationService
                            .getPropertyValueAsString(KFSKeyConstants.ERROR_COST_SHARE_OBJECT_NOT_FOUND),
                    costShareEntry.getFinancialObjectCode(), Message.TYPE_FATAL);
        }
    }

    /**
     * The purpose of this method is to build the actual offset transaction. It does this by performing the following steps: 1.
     * Getting the offset object code and offset subobject code from the GL Offset Definition Table. 2. For the offset object code
     * it needs to get the associated object type, object subtype, and object active code.
     *
     * @param scrubbedEntry entry to determine if an offset is needed for
     * @return true if an offset would be needed for this entry, false otherwise
     */
    protected boolean generateOffset(OriginEntryInformation scrubbedEntry, ScrubberReportData scrubberReport) {
        OriginEntryFull offsetEntry = new OriginEntryFull();
        try {
            // This code is 3000-OFFSET to SET-OBJECT-2004 in the Cobol
            LOG.debug("generateOffset() started");

            // There was no previous unit of work so we need no offset
            if (scrubbedEntry == null) {
                return true;
            }

            // If there was an error, don't generate an offset since the record was pulled
            // and the rest of the document's records will be demerged
            if (unitOfWork.errorsFound == true) {
                return true;
            }

            // If the offset amount is zero, don't bother to lookup the offset definition ...
            if (unitOfWork.offsetAmount.isZero()) {
                return true;
            }

            ParameterEvaluator docTypeRule = /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class)
                    .getParameterEvaluator(ScrubberStep.class,
                            GeneralLedgerConstants.GlScrubberGroupRules.OFFSET_DOC_TYPE_CODES,
                            scrubbedEntry.getFinancialDocumentTypeCode());
            if (!docTypeRule.evaluationSucceeds()) {
                return true;
            }

            // do nothing if flexible offset is enabled and scrubber offset indicator of the document
            // type code is turned off in the document type table
            if (flexibleOffsetAccountService.getEnabled()
                    && !shouldScrubberGenerateOffsetsForDocType(scrubbedEntry.getFinancialDocumentTypeCode())) {
                return true;
            }

            // Create an offset
            offsetEntry = OriginEntryFull.copyFromOriginEntryable(scrubbedEntry);
            offsetEntry.setTransactionLedgerEntryDescription(offsetDescription);

            //of course this method should go elsewhere, not in ScrubberValidator!
            OffsetDefinition offsetDefinition = accountingCycleCachingService.getOffsetDefinition(
                    scrubbedEntry.getUniversityFiscalYear(), scrubbedEntry.getChartOfAccountsCode(),
                    scrubbedEntry.getFinancialDocumentTypeCode(), scrubbedEntry.getFinancialBalanceTypeCode());
            if (offsetDefinition != null) {
                if (offsetDefinition.getFinancialObject() == null) {
                    StringBuffer offsetKey = new StringBuffer(offsetDefinition.getUniversityFiscalYear());
                    offsetKey.append("-");
                    offsetKey.append(offsetDefinition.getChartOfAccountsCode());
                    offsetKey.append("-");
                    offsetKey.append(offsetDefinition.getFinancialObjectCode());

                    putTransactionError(offsetEntry,
                            configurationService.getPropertyValueAsString(
                                    KFSKeyConstants.ERROR_OFFSET_DEFINITION_OBJECT_CODE_NOT_FOUND),
                            offsetKey.toString(), Message.TYPE_FATAL);

                    createOutputEntry(offsetEntry, OUTPUT_ERR_FILE_ps);
                    scrubberReport.incrementErrorRecordWritten();
                    return false;
                }

                offsetEntry.setFinancialObject(offsetDefinition.getFinancialObject());
                offsetEntry.setFinancialObjectCode(offsetDefinition.getFinancialObjectCode());

                offsetEntry.setFinancialSubObject(null);
                offsetEntry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
            } else {
                StringBuffer sb = new StringBuffer("Unit of work offset ");
                sb.append(scrubbedEntry.getUniversityFiscalYear());
                sb.append("-");
                sb.append(scrubbedEntry.getChartOfAccountsCode());
                sb.append("-");
                sb.append(scrubbedEntry.getFinancialDocumentTypeCode());
                sb.append("-");
                sb.append(scrubbedEntry.getFinancialBalanceTypeCode());

                putTransactionError(offsetEntry,
                        configurationService
                                .getPropertyValueAsString(KFSKeyConstants.ERROR_OFFSET_DEFINITION_NOT_FOUND),
                        sb.toString(), Message.TYPE_FATAL);

                createOutputEntry(offsetEntry, OUTPUT_ERR_FILE_ps);
                scrubberReport.incrementErrorRecordWritten();
                return false;
            }

            offsetEntry.setFinancialObjectTypeCode(offsetEntry.getFinancialObject().getFinancialObjectTypeCode());
            offsetEntry.setTransactionLedgerEntryAmount(unitOfWork.offsetAmount);

            if (unitOfWork.offsetAmount.isPositive()) {
                offsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
            } else {
                offsetEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
                offsetEntry.setTransactionLedgerEntryAmount(unitOfWork.offsetAmount.negated());
            }

            offsetEntry.setOrganizationDocumentNumber(null);
            offsetEntry.setOrganizationReferenceId(null);
            offsetEntry.setReferenceFinancialDocumentTypeCode(null);
            offsetEntry.setReferenceFinancialSystemOriginationCode(null);
            offsetEntry.setReferenceFinancialDocumentNumber(null);
            offsetEntry.setTransactionEncumbranceUpdateCode(null);
            offsetEntry.setProjectCode(KFSConstants.getDashProjectCode());
            offsetEntry.setTransactionDate(getTransactionDateForOffsetEntry(scrubbedEntry));

            try {
                flexibleOffsetAccountService.updateOffset(offsetEntry);
            } catch (InvalidFlexibleOffsetException e) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("generateOffset() Offset Flexible Offset Error: " + e.getMessage());
                }
                putTransactionError(offsetEntry, e.getMessage(), "", Message.TYPE_FATAL);
                return true;
            }

            createOutputEntry(offsetEntry, OUTPUT_GLE_FILE_ps);
            scrubberReport.incrementOffsetEntryGenerated();

        } catch (IOException ioe) {
            LOG.error("generateOffset() Stopped: " + ioe.getMessage());
            throw new RuntimeException("generateOffset() Stopped: " + ioe.getMessage(), ioe);
        }

        return true;
    }

    protected void createOutputEntry(OriginEntryInformation entry, PrintStream ps) throws IOException {
        try {
            ps.printf("%s\n", entry.getLine());
        } catch (Exception e) {
            throw new IOException(e.toString());
        }
    }

    protected void createOutputEntry(String line, PrintStream ps) throws IOException {
        try {
            ps.printf("%s\n", line);
        } catch (Exception e) {
            throw new IOException(e.toString());
        }
    }

    /**
     * Add an error message to the list of messages for this transaction
     *
     * @param errorMessage Error message
     * @param errorValue Value that is in error
     * @param type Type of error (Fatal or Warning)
     */
    protected void addTransactionError(String errorMessage, String errorValue, int type) {
        transactionErrors.add(new Message(errorMessage + " (" + errorValue + ")", type));
    }

    /**
     * Puts a transaction error into this instance's collection of errors
     *
     * @param s a transaction that caused a scrubber error
     * @param errorMessage the message of what caused the error
     * @param errorValue the value in error
     * @param type the type of error
     */
    protected void putTransactionError(Transaction s, String errorMessage, String errorValue, int type) {
        Message m = new Message(errorMessage + "(" + errorValue + ")", type);
        scrubberReportWriterService.writeError(s, m);
    }

    /**
     * Determines if the scrubber should generate offsets for the given document type
     * @param docTypeCode the document type code to check if it generates scrubber offsets
     * @return true if the scrubber should generate offsets for this doc type, false otherwise
     */
    protected boolean shouldScrubberGenerateOffsetsForDocType(String docTypeCode) {
        return /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(
                ScrubberStep.class,
                GeneralLedgerConstants.GlScrubberGroupRules.DOCUMENT_TYPES_REQUIRING_FLEXIBLE_OFFSET_BALANCING_ENTRIES,
                docTypeCode).evaluationSucceeds();
    }

    /**
     * A class to hold the current unit of work the scrubber is using
     */
    class UnitOfWorkInfo {
        // Unit of work key
        public Integer univFiscalYr = 0;
        public String finCoaCd = "";
        public String accountNbr = "";
        public String subAcctNbr = "";
        public String finBalanceTypCd = "";
        public String fdocTypCd = "";
        public String fsOriginCd = "";
        public String fdocNbr = "";
        public Date fdocReversalDt = new Date(dateTimeService.getCurrentDate().getTime());
        public String univFiscalPrdCd = "";

        // Data about unit of work
        public boolean errorsFound = false;
        public KualiDecimal offsetAmount = KualiDecimal.ZERO;
        public String scrbFinCoaCd;
        public String scrbAccountNbr;

        /**
         * Constructs a ScrubberProcess.UnitOfWorkInfo instance
         */
        public UnitOfWorkInfo() {
        }

        /**
         * Constructs a ScrubberProcess.UnitOfWorkInfo instance
         * @param e an origin entry belonging to this unit of work
         */
        public UnitOfWorkInfo(OriginEntryInformation e) {
            univFiscalYr = e.getUniversityFiscalYear();
            finCoaCd = e.getChartOfAccountsCode();
            accountNbr = e.getAccountNumber();
            subAcctNbr = e.getSubAccountNumber();
            finBalanceTypCd = e.getFinancialBalanceTypeCode();
            fdocTypCd = e.getFinancialDocumentTypeCode();
            fsOriginCd = e.getFinancialSystemOriginationCode();
            fdocNbr = e.getDocumentNumber();
            fdocReversalDt = e.getFinancialDocumentReversalDate();
            univFiscalPrdCd = e.getUniversityFiscalPeriodCode();
        }

        /**
         * Determines if an entry belongs to this unit of work
         *
         * @param e the entry to check
         * @return true if it belongs to this unit of work, false otherwise
         */
        public boolean isSameUnitOfWork(OriginEntryInformation e) {
            // Compare the key fields
            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());
        }

        /**
         * Converts this unit of work info to a String
         * @return a String representation of this UnitOfWorkInfo
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return univFiscalYr + finCoaCd + accountNbr + subAcctNbr + finBalanceTypCd + fdocTypCd + fsOriginCd
                    + fdocNbr + fdocReversalDt + univFiscalPrdCd;
        }

        /**
         * Generates the beginning of an OriginEntryFull, based on the unit of work info
         *
         * @return a partially initialized OriginEntryFull
         */
        public OriginEntryFull getOffsetTemplate() {
            OriginEntryFull e = new OriginEntryFull();
            e.setUniversityFiscalYear(univFiscalYr);
            e.setChartOfAccountsCode(finCoaCd);
            e.setAccountNumber(accountNbr);
            e.setSubAccountNumber(subAcctNbr);
            e.setFinancialBalanceTypeCode(finBalanceTypCd);
            e.setFinancialDocumentTypeCode(fdocTypCd);
            e.setFinancialSystemOriginationCode(fsOriginCd);
            e.setDocumentNumber(fdocNbr);
            e.setFinancialDocumentReversalDate(fdocReversalDt);
            e.setUniversityFiscalPeriodCode(univFiscalPrdCd);
            return e;
        }
    }

    /**
     * An internal class to hold errors encountered by the scrubber
     */
    class TransactionError {
        public Transaction transaction;
        public Message message;

        /**
         * Constructs a ScrubberProcess.TransactionError instance
         * @param t the transaction that had the error
         * @param m a message about the error
         */
        public TransactionError(Transaction t, Message m) {
            transaction = t;
            message = m;
        }
    }

    /**
     * This method modifies the run date if it is before the cutoff time specified by the RunTimeService See
     * KULRNE-70 This method is public to facilitate unit testing
     *
     * @param currentDate the date the scrubber should report as having run on
     * @return the run date
     */
    @Override
    public Date calculateRunDate(java.util.Date currentDate) {
        return new Date(runDateService.calculateRunDate(currentDate).getTime());
    }

    protected boolean checkingBypassEntry(String financialBalanceTypeCode, String desc,
            DemergerReportData demergerReport) {
        String transactionType = getTransactionType(financialBalanceTypeCode, desc);

        if (TRANSACTION_TYPE_COST_SHARE_ENCUMBRANCE.equals(transactionType)) {
            demergerReport.incrementCostShareEncumbranceTransactionsBypassed();
            return true;
        } else if (TRANSACTION_TYPE_OFFSET.equals(transactionType)) {
            demergerReport.incrementOffsetTransactionsBypassed();
            return true;
        } else if (TRANSACTION_TYPE_CAPITALIZATION.equals(transactionType)) {
            demergerReport.incrementCapitalizationTransactionsBypassed();
            return true;
        } else if (TRANSACTION_TYPE_LIABILITY.equals(transactionType)) {
            demergerReport.incrementLiabilityTransactionsBypassed();
            return true;
        } else if (TRANSACTION_TYPE_TRANSFER.equals(transactionType)) {
            demergerReport.incrementTransferTransactionsBypassed();
            return true;
        } else if (TRANSACTION_TYPE_COST_SHARE.equals(transactionType)) {
            demergerReport.incrementCostShareTransactionsBypassed();
            return true;
        }

        return false;
    }

    protected String checkAndSetTransactionTypeCostShare(String financialBalanceTypeCode, String desc,
            String currentValidLine) {

        // Read all the transactions in the valid group and update the cost share transactions
        String transactionType = getTransactionType(financialBalanceTypeCode, desc);
        if (TRANSACTION_TYPE_COST_SHARE.equals(transactionType)) {
            OriginEntryFull transaction = new OriginEntryFull();
            transaction.setFromTextFileForBatch(currentValidLine, 0);

            transaction.setFinancialDocumentTypeCode(KFSConstants.TRANSFER_FUNDS);
            transaction.setFinancialSystemOriginationCode(KFSConstants.SubAccountType.COST_SHARE);
            StringBuffer docNbr = new StringBuffer(COST_SHARE_CODE);

            docNbr.append(desc.substring(36, 38));
            docNbr.append("/");
            docNbr.append(desc.substring(38, 40));
            transaction.setDocumentNumber(docNbr.toString());
            transaction.setTransactionLedgerEntryDescription(
                    desc.substring(0, DEMERGER_TRANSACTION_LEDGET_ENTRY_DESCRIPTION));

            currentValidLine = transaction.getLine();
        }

        return currentValidLine;

    }

    /**
     * Generates the scrubber listing report for the GLCP document
     * @param documentNumber the document number of the GLCP document
     */
    protected void generateScrubberTransactionListingReport(String documentNumber, String inputFileName) {
        try {
            scrubberListingReportWriterService.setDocumentNumber(documentNumber);
            ((WrappingBatchService) scrubberListingReportWriterService).initialize();
            new TransactionListingReport().generateReport(scrubberListingReportWriterService,
                    new OriginEntryFileIterator(new File(inputFileName)));
        } finally {
            ((WrappingBatchService) scrubberListingReportWriterService).destroy();
        }
    }

    /**
     * Generates the scrubber report that lists out the input origin entries with blank balance type codes.
     */
    protected void generateScrubberBlankBalanceTypeCodeReport(String inputFileName) {
        OriginEntryFilter blankBalanceTypeFilter = new OriginEntryFilter() {
            /**
             * @see org.kuali.kfs.gl.batch.service.impl.FilteringOriginEntryFileIterator.OriginEntryFilter#accept(org.kuali.kfs.gl.businessobject.OriginEntryFull)
             */
            @Override
            public boolean accept(OriginEntryFull originEntry) {
                boolean acceptFlag = false;
                String financialBalancetype = originEntry.getFinancialBalanceTypeCode();
                BalanceType originEntryBalanceType = accountingCycleCachingService
                        .getBalanceType(financialBalancetype);
                if (ObjectUtils.isNull(originEntryBalanceType)) {
                    acceptFlag = true;
                    for (int i = 0; i < financialBalancetype.length(); i++) {
                        if (financialBalancetype.charAt(i) != ' ') {
                            acceptFlag = false;
                            break;
                        }
                    }
                }
                return acceptFlag;
            }
        };
        Iterator<OriginEntryFull> blankBalanceOriginEntries = new FilteringOriginEntryFileIterator(
                new File(inputFileName), blankBalanceTypeFilter);
        new TransactionListingReport().generateReport(scrubberBadBalanceListingReportWriterService,
                blankBalanceOriginEntries);
    }

    protected void generateDemergerRemovedTransactionsReport(String errorFileName) {
        OriginEntryFileIterator removedTransactions = new OriginEntryFileIterator(new File(errorFileName));
        new TransactionListingReport().generateReport(demergerRemovedTransactionsListingReportWriterService,
                removedTransactions);
    }

    protected void handleTransactionError(Transaction errorTransaction, Message message) {
        if (collectorMode) {
            List<Message> messages = scrubberReportErrors.get(errorTransaction);
            if (messages == null) {
                messages = new ArrayList<Message>();
                scrubberReportErrors.put(errorTransaction, messages);
            }
            messages.add(message);
        } else {
            scrubberReportWriterService.writeError(errorTransaction, message);
        }
    }

    protected void handleTransactionErrors(Transaction errorTransaction, List<Message> messages) {
        if (collectorMode) {
            for (Message message : messages) {
                handleTransactionError(errorTransaction, message);
            }
        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Errors on transaction: " + errorTransaction);
                for (Message message : messages) {
                    LOG.debug(message);
                }
            }
            scrubberReportWriterService.writeError(errorTransaction, messages);
        }
    }

    protected void handleEndOfScrubberReport(ScrubberReportData scrubberReport) {
        if (!collectorMode) {
            scrubberReportWriterService.writeStatisticLine("UNSCRUBBED RECORDS READ              %,9d",
                    scrubberReport.getNumberOfUnscrubbedRecordsRead());
            scrubberReportWriterService.writeStatisticLine("SCRUBBED RECORDS WRITTEN             %,9d",
                    scrubberReport.getNumberOfScrubbedRecordsWritten());
            scrubberReportWriterService.writeStatisticLine("ERROR RECORDS WRITTEN                %,9d",
                    scrubberReport.getNumberOfErrorRecordsWritten());
            scrubberReportWriterService.writeStatisticLine("OFFSET ENTRIES GENERATED             %,9d",
                    scrubberReport.getNumberOfOffsetEntriesGenerated());
            scrubberReportWriterService.writeStatisticLine("CAPITALIZATION ENTRIES GENERATED     %,9d",
                    scrubberReport.getNumberOfCapitalizationEntriesGenerated());
            scrubberReportWriterService.writeStatisticLine("LIABILITY ENTRIES GENERATED          %,9d",
                    scrubberReport.getNumberOfLiabilityEntriesGenerated());
            scrubberReportWriterService.writeStatisticLine("PLANT INDEBTEDNESS ENTRIES GENERATED %,9d",
                    scrubberReport.getNumberOfPlantIndebtednessEntriesGenerated());
            scrubberReportWriterService.writeStatisticLine("COST SHARE ENTRIES GENERATED         %,9d",
                    scrubberReport.getNumberOfCostShareEntriesGenerated());
            scrubberReportWriterService.writeStatisticLine("COST SHARE ENC ENTRIES GENERATED     %,9d",
                    scrubberReport.getNumberOfCostShareEncumbrancesGenerated());
            scrubberReportWriterService.writeStatisticLine("TOTAL OUTPUT RECORDS WRITTEN         %,9d",
                    scrubberReport.getTotalNumberOfRecordsWritten());
            scrubberReportWriterService.writeStatisticLine("EXPIRED ACCOUNTS FOUND               %,9d",
                    scrubberReport.getNumberOfExpiredAccountsFound());
        }
    }

    protected void handleDemergerSaveValidEntry(String entryString) {
        if (collectorMode) {
            OriginEntryInformation tempEntry = new OriginEntryFull(entryString);
            ledgerSummaryReport.summarizeEntry(tempEntry);
        }
    }

    /**
     * Sets the batchFileDirectoryName attribute value.
     * @param batchFileDirectoryName The batchFileDirectoryName to set.
     */
    public void setBatchFileDirectoryName(String batchFileDirectoryName) {
        this.batchFileDirectoryName = batchFileDirectoryName;
    }

    /**
     * Gets the transferDescription attribute.
     * @return Returns the transferDescription.
     */
    public String getTransferDescription() {
        return transferDescription;
    }

    /**
     * Sets the transferDescription attribute value.
     * @param transferDescription The transferDescription to set.
     */
    public void setTransferDescription(String transferDescription) {
        this.transferDescription = transferDescription;
    }

    /**
     * Sets the dateTimeService attribute value.
     * @param dateTimeService The dateTimeService to set.
     */
    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    /**
     * Sets the flexibleOffsetAccountService attribute value.
     * @param flexibleOffsetAccountService The flexibleOffsetAccountService to set.
     */
    public void setFlexibleOffsetAccountService(FlexibleOffsetAccountService flexibleOffsetAccountService) {
        this.flexibleOffsetAccountService = flexibleOffsetAccountService;
    }

    /**
     * Sets the configurationService attribute value.
     * @param configurationService The configurationService to set.
     */
    public void setConfigurationService(ConfigurationService configurationService) {
        this.configurationService = configurationService;
    }

    /**
     * Sets the persistenceService attribute value.
     * @param persistenceService The persistenceService to set.
     */
    public void setPersistenceService(PersistenceService persistenceService) {
        this.persistenceService = persistenceService;
    }

    /**
     * Sets the scrubberValidator attribute value.
     * @param scrubberValidator The scrubberValidator to set.
     */
    public void setScrubberValidator(ScrubberValidator scrubberValidator) {
        this.scrubberValidator = scrubberValidator;
    }

    /**
     * Sets the accountingCycleCachingService attribute value.
     * @param accountingCycleCachingService The accountingCycleCachingService to set.
     */
    public void setAccountingCycleCachingService(AccountingCycleCachingService accountingCycleCachingService) {
        this.accountingCycleCachingService = accountingCycleCachingService;
    }

    /**
     * Sets the scrubberReportWriterService attribute value.
     * @param scrubberReportWriterService The scrubberReportWriterService to set.
     */
    public void setScrubberReportWriterService(DocumentNumberAwareReportWriterService scrubberReportWriterService) {
        this.scrubberReportWriterService = scrubberReportWriterService;
    }

    /**
     * Sets the scrubberLedgerReportWriterService attribute value.
     * @param scrubberLedgerReportWriterService The scrubberLedgerReportWriterService to set.
     */
    public void setScrubberLedgerReportWriterService(
            DocumentNumberAwareReportWriterService scrubberLedgerReportWriterService) {
        this.scrubberLedgerReportWriterService = scrubberLedgerReportWriterService;
    }

    /**
     * Sets the scrubberListingReportWriterService attribute value.
     * @param scrubberListingReportWriterService The scrubberListingReportWriterService to set.
     */
    public void setScrubberListingReportWriterService(
            DocumentNumberAwareReportWriterService scrubberListingReportWriterService) {
        this.scrubberListingReportWriterService = scrubberListingReportWriterService;
    }

    /**
     * Sets the scrubberBadBalanceListingReportWriterService attribute value.
     * @param scrubberBadBalanceListingReportWriterService The scrubberBadBalanceListingReportWriterService to set.
     */
    public void setScrubberBadBalanceListingReportWriterService(
            ReportWriterService scrubberBadBalanceListingReportWriterService) {
        this.scrubberBadBalanceListingReportWriterService = scrubberBadBalanceListingReportWriterService;
    }

    /**
     * Sets the demergerRemovedTransactionsListingReportWriterService attribute value.
     * @param demergerRemovedTransactionsListingReportWriterService The demergerRemovedTransactionsListingReportWriterService to set.
     */
    public void setDemergerRemovedTransactionsListingReportWriterService(
            ReportWriterService demergerRemovedTransactionsListingReportWriterService) {
        this.demergerRemovedTransactionsListingReportWriterService = demergerRemovedTransactionsListingReportWriterService;
    }

    /**
     * Sets the demergerReportWriterService attribute value.
     * @param demergerReportWriterService The demergerReportWriterService to set.
     */
    public void setDemergerReportWriterService(ReportWriterService demergerReportWriterService) {
        this.demergerReportWriterService = demergerReportWriterService;
    }

    /**
     * Sets the preScrubberService attribute value.
     * @param preScrubberService The preScrubberService to set.
     */
    public void setPreScrubberService(PreScrubberService preScrubberService) {
        this.preScrubberService = preScrubberService;
    }

    /**
     * Sets the parameterService attribute value.
     * @param parameterService The parameterService to set.
     */
    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    /**
     * Sets the runDateService attribute value.
     * @param runDateService The runDateService to set.
     */
    public void setRunDateService(RunDateService runDateService) {
        this.runDateService = runDateService;
    }

    /**
     * Gets the flexibleOffsetAccountService attribute.
     * @return Returns the flexibleOffsetAccountService.
     */
    public FlexibleOffsetAccountService getFlexibleOffsetAccountService() {
        return flexibleOffsetAccountService;
    }

    /**
     * Gets the dateTimeService attribute.
     * @return Returns the dateTimeService.
     */
    public DateTimeService getDateTimeService() {
        return dateTimeService;
    }

    /**
     * Gets the configurationService attribute.
     * @return Returns the configurationService.
     */
    public ConfigurationService getConfigurationService() {
        return configurationService;
    }

    /**
     * Gets the persistenceService attribute.
     * @return Returns the persistenceService.
     */
    public PersistenceService getPersistenceService() {
        return persistenceService;
    }

    /**
     * Gets the scrubberValidator attribute.
     * @return Returns the scrubberValidator.
     */
    public ScrubberValidator getScrubberValidator() {
        return scrubberValidator;
    }

    /**
     * Gets the runDateService attribute.
     * @return Returns the runDateService.
     */
    public RunDateService getRunDateService() {
        return runDateService;
    }

    /**
     * Gets the accountingCycleCachingService attribute.
     * @return Returns the accountingCycleCachingService.
     */
    public AccountingCycleCachingService getAccountingCycleCachingService() {
        return accountingCycleCachingService;
    }

    /**
     * Gets the scrubberReportWriterService attribute.
     * @return Returns the scrubberReportWriterService.
     */
    public DocumentNumberAwareReportWriterService getScrubberReportWriterService() {
        return scrubberReportWriterService;
    }

    /**
     * Gets the scrubberLedgerReportWriterService attribute.
     * @return Returns the scrubberLedgerReportWriterService.
     */
    public DocumentNumberAwareReportWriterService getScrubberLedgerReportWriterService() {
        return scrubberLedgerReportWriterService;
    }

    /**
     * Gets the scrubberListingReportWriterService attribute.
     * @return Returns the scrubberListingReportWriterService.
     */
    public DocumentNumberAwareReportWriterService getScrubberListingReportWriterService() {
        return scrubberListingReportWriterService;
    }

    /**
     * Gets the scrubberBadBalanceListingReportWriterService attribute.
     * @return Returns the scrubberBadBalanceListingReportWriterService.
     */
    public ReportWriterService getScrubberBadBalanceListingReportWriterService() {
        return scrubberBadBalanceListingReportWriterService;
    }

    /**
     * Gets the demergerRemovedTransactionsListingReportWriterService attribute.
     * @return Returns the demergerRemovedTransactionsListingReportWriterService.
     */
    public ReportWriterService getDemergerRemovedTransactionsListingReportWriterService() {
        return demergerRemovedTransactionsListingReportWriterService;
    }

    /**
     * Gets the demergerReportWriterService attribute.
     * @return Returns the demergerReportWriterService.
     */
    public ReportWriterService getDemergerReportWriterService() {
        return demergerReportWriterService;
    }

    /**
     * Gets the preScrubberService attribute.
     * @return Returns the preScrubberService.
     */
    public PreScrubberService getPreScrubberService() {
        return preScrubberService;
    }

    /**
     * Gets the parameterService attribute.
     * @return Returns the parameterService.
     */
    public ParameterService getParameterService() {
        return parameterService;
    }

    /**
     * Sets the preScrubberReportWriterService attribute value.
     * @param preScrubberReportWriterService The preScrubberReportWriterService to set.
     */
    public void setPreScrubberReportWriterService(
            DocumentNumberAwareReportWriterService preScrubberReportWriterService) {
        this.preScrubberReportWriterService = preScrubberReportWriterService;
    }

    /**
     * Sets the businessObjectService attribute value.
     * @param businessObjectService The businessObjectService to set.
     */
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    // Offset entry to have the same transaction date as the original transaction for Payroll Posting
    protected Date getTransactionDateForOffsetEntry(OriginEntryInformation scrubbedEntry) {
        if (getParameterService().parameterExists(ScrubberStep.class,
                KFSParameterKeyConstants.GeneralLedgerSysParmeterKeys.TRANSACTION_DATE_BYPASS_ORIGINATIONS)) {
            Collection<String> transactionDateAutoAssignmentBypassOriginCodes = getParameterService()
                    .getParameterValuesAsString(ScrubberStep.class,
                            KFSParameterKeyConstants.GeneralLedgerSysParmeterKeys.TRANSACTION_DATE_BYPASS_ORIGINATIONS);
            String originationCode = scrubbedEntry.getFinancialSystemOriginationCode();
            if (transactionDateAutoAssignmentBypassOriginCodes.contains(originationCode)) {
                return scrubbedEntry.getTransactionDate();
            }
        }

        return this.runDate;
    }

}